Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

icalendar

Package Overview
Dependencies
Maintainers
2
Versions
18
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

icalendar - npm Package Compare versions

Comparing version 0.4.1 to 0.5.0

lib/base.js

362

lib/icalendar.js

@@ -30,184 +30,8 @@ // Copyright (C) 2011 Tri Tech Computers Ltd.

var types = require('./types');
var parser = require('./parser');
var CalendarObject = require('./base').CalendarObject;
var CalendarProperty = require('./base').CalendarProperty;
var schema = require('./base').schema;
var format_value = exports.format_value = types.format_value;
var parse_value = exports.parse_value = types.parse_value;
var RRule = exports.RRule = require('./recur').RRule;
// Maximum number of octets in a single iCalendar line
var MAX_LINE = 75;
exports.PRODID = '-//Tri Tech Computers//node-icalendar//EN';
exports.parse_calendar = parser.parse_calendar;
var CalendarObject = exports.CalendarObject = function(calendar, element) {
this.calendar = calendar;
this.element = element;
this.components = {};
this.properties = {};
}
// Create an element of the correct type
CalendarObject.create = function(element, calendar) {
var factory = (schema[element] || {}).factory;
return (factory !== undefined
? new factory(calendar)
: new CalendarObject(calendar, comp));
}
// Recursively generates a clone of some calendar object
CalendarObject.prototype.clone = function() {
var obj = CalendarObject.create(this.element, this.calendar);
for(var prop in this.properties)
obj.addProperty(this.properties[prop]);
var comp = this.getComponents();
for(var i=0; i<comp.length; ++i)
obj.addComponent(comp[i]);
return obj;
}
CalendarObject.prototype.addProperty = function(prop, value, parameters) {
if(!(prop instanceof CalendarProperty))
prop = new CalendarProperty(prop, value, parameters);
else
prop = prop.clone();
// TODO: What about multiple occurances of the same property?
this.properties[prop.name] = prop;
return prop;
}
CalendarObject.prototype.addComponent = function(comp) {
if(!(comp instanceof CalendarObject)) {
var factory = (schema[comp] || {}).factory;
comp = factory !== undefined
? new factory(this.calendar)
: new CalendarObject(this.calendar, comp);
}
// Create a copy of the component if it's from a different
// calendar object to prevent changes from one place happening
// somewhere else as well
if(comp.calendar && comp.calendar !== this.calendar)
comp = comp.clone();
this.components[comp.element] = this.components[comp.element] || [];
this.components[comp.element].push(comp);
comp.calendar = this.calendar;
return comp;
}
CalendarObject.prototype.getComponents = function(type) {
if(type === undefined) {
var all = [];
for(var c in this.components)
all = all.concat(this.components[c]);
return all;
}
return this.components[type] || [];
}
CalendarObject.prototype.getProperty = function(prop) {
return this.properties[prop];
}
CalendarObject.prototype.getPropertyValue = function(prop) {
return (this.properties[prop] || {}).value;
}
CalendarObject.prototype.toString = function() {
// Make sure output always includes a VCALENDAR object
var output;
if(this.element == 'VCALENDAR')
output = this.format();
else {
var ical = new iCalendar();
ical.addComponent(this);
output = ical.format()
}
output.push(''); // <-- Add empty element to ensure trailing CRLF
return output.join('\r\n');
}
CalendarObject.prototype.format = function() {
var lines = ['BEGIN:'+this.element];
for(var i in this.properties)
lines.push.apply(lines, this.properties[i].format());
for(var comp in this.components) {
var comp = this.components[comp];
for(var i=0; i < comp.length; ++i)
lines.push.apply(lines, comp[i].format());
}
lines.push('END:'+this.element);
return lines;
}
var CalendarProperty = exports.CalendarProperty = function(name, value, parameters) {
var propdef = properties[name];
this.type = propdef && propdef.type ? propdef.type : 'TEXT';
this.name = name;
this.value = value;
this.parameters = parameters || {};
}
CalendarProperty.prototype.clone = function() {
var obj = new CalendarProperty(this.name, this.value);
obj.type = this.type;
// TODO: Copy type and value instances in the case of objects, dates, arrays
for(var param in this.parameters)
obj.parameters[param] = this.parameters[param];
return obj;
}
CalendarProperty.prototype.getParameter = function(param) {
return this.parameters[param];
}
CalendarProperty.prototype.format = function() {
var data = new Buffer(this.name+':'+format_value(this.type, this.value));
var pos = 0, len;
var output = [];
while(true) {
len = MAX_LINE;
if(pos+len >= data.length)
len = data.length-pos;
// We're in the middle of a unicode character if the high bit is set and
// the next byte is 10xxxxxx (or 0x80). Don't split it in half.
// Wind backward until we find the start character...
while((data[pos+len] & 0xc0) == 0x80)
len--;
output.push(data.toString('utf8', pos, pos+len));
if(pos+len >= data.length)
break;
// Insert the space for the start of the next line...
pos += len-1;
data[pos] = 0x20;
}
return output;
}
var iCalendar = exports.iCalendar = function(empty) {

@@ -218,3 +42,3 @@ CalendarObject.call(this, this, 'VCALENDAR');

if(!empty) {
this.addProperty('PRODID', exports.PRODID);
this.addProperty('PRODID', require('./index').PRODID);
this.addProperty('VERSION', '2.0');

@@ -225,4 +49,2 @@ }

iCalendar.parse = parser.parse_calendar;
iCalendar.prototype.events = function() { return this.components['VEVENT'] || []; }

@@ -239,163 +61,23 @@

var VEvent = exports.VEvent = function(calendar, uid) {
if(!(calendar instanceof iCalendar)) {
uid = calendar;
calendar = null;
}
CalendarObject.call(this, calendar, 'VEVENT');
// TODO: Move validation to its own method
// if(uid === undefined)
// throw Error("UID is a required parameter");
if(uid !== undefined) {
this.addProperty('DTSTAMP', new Date());
this.addProperty('UID', uid);
}
}
util.inherits(VEvent, CalendarObject);
VEvent.prototype.setSummary = function(summ) {
this.addProperty('SUMMARY', summ);
}
VEvent.prototype.setDescription = function(desc) {
this.addProperty('DESCRIPTION', desc);
}
VEvent.prototype.setDate = function(start, end) {
this.addProperty('DTSTART', start);
if(end instanceof Date)
this.addProperty('DTEND', end);
else
this.addProperty('DURATION', end);
}
VEvent.prototype.inTimeRange = function(start, end) {
var dtstart = this.getPropertyValue('DTSTART');
var dtend = this.getPropertyValue('DTEND');
var rr = this.getPropertyValue('RRULE');
if(rr) {
rr = new RRule(rr, dtstart, dtend);
var next = rr.next(start);
return (next !== null && (!end || next <= end));
}
if(!dtend) {
var duration = this.getPropertyValue('DURATION');
if(duration === 0)
// Special case for zero-duration, as per RFC4791
return (!start || start <= dtstart) && (!end || end > dtstart);
else if(duration)
dtend = new Date(dtstart.valueOf() + this.getPropertyValue('DURATION')*1000);
else
dtend = new Date(dtstart.valueOf() + 24*60*60*1000); // +1 day
}
return (!start || start < dtend) && (!end || end > dtstart);
}
var VTimezone = exports.VTimezone = function(calendar, tzid) {
CalendarObject.call(this, calendar, 'VTIMEZONE');
this.addProperty('TZID', tzid);
}
util.inherits(VTimezone, CalendarObject);
VTimezone.prototype.getRRule = function(section) {
var comp = this.getComponents('STANDARD')[0];
if(!comp || !comp.getPropertyValue('RRULE'))
return null;
return new RRule(comp.getPropertyValue('RRULE'),
comp.getPropertyValue('DTSTART'),
comp.getPropertyValue('DTEND'));
}
VTimezone.prototype.getOffsetForDate = function(dt) {
if(!this.getComponents('DAYLIGHT').length)
return this.getComponents('STANDARD').getPropertyValue('TZOFFSETTO');
// Right now we're only supporting a single element
assert.equal(1, this.components['STANDARD'].length);
assert.equal(1, this.components['DAYLIGHT'].length);
var next_std = this.getRRule('STANDARD').next(dt);
var next_dst = this.getRRule('DAYLIGHT').next(dt);
// TODO: Using prevOccurs would be a better solution
return this.getComponents(next_std < next_dst ? 'STANDARD' : 'DAYLIGHT')[0]
.getPropertyValue('TZOFFSETTO');
}
// Convert a parsed date in localtime to a UTC date object
VTimezone.prototype.fromLocalTime = function(dtarray) {
// Create a slightly inaccurate date object
var dt = new Date(dtarray[0], dtarray[1]-1, dtarray[2],
dtarray[3], dtarray[4], dtarray[5]);
var hrs = this.getOffsetForDate(dt);
var min = hrs % 100;
hrs = (hrs-min) / 100;
return new Date(Date.UTC(dtarray[0], dtarray[1]-1, dtarray[2],
dtarray[3]-hrs, dtarray[4]-min, dtarray[5]));
}
// iCalendar schema, required prop
var schema = exports.schema = {
VCALENDAR: {
factory: iCalendar,
valid_properties: [],
required_properties: ['PRODID','VERSION'],
valid_children: ['VEVENT'],
required_children: []
},
VEVENT: {
factory: VEvent,
valid_properties: [],
required_properties: ['DTSTAMP','UID'],
valid_children: [],
required_children: []
},
VTODO: {
required_properties: ['DTSTAMP','UID']
},
VJOURNAL: {
required_properties: ['DTSTAMP','UID']
},
VFREEBUSY: {
required_properties: ['DTSTAMP','UID']
},
VALARM: {
required_properties: ['ACTION','TRIGGER']
},
VTIMEZONE: {
factory: VTimezone,
}
schema.VCALENDAR = {
factory: iCalendar,
valid_properties: [],
required_properties: ['PRODID','VERSION'],
valid_children: ['VEVENT'],
required_children: []
};
var properties = exports.properties = {
DTSTAMP: {
type: 'DATE-TIME'
},
DTSTART: {
params: ['TZID'],
type: 'DATE-TIME'
},
DTEND: {
type: 'DATE-TIME'
},
DURATION: {
type: 'DURATION'
},
RRULE: {
type: 'RECUR'
},
VERSION: { },
PRODID: { }
// Unimplemented components...
schema.VTODO = {
required_properties: ['DTSTAMP','UID']
};
schema.VJOURNAL = {
required_properties: ['DTSTAMP','UID']
};
schema.VFREEBUSY = {
required_properties: ['DTSTAMP','UID']
};
schema.VALARM = {
required_properties: ['ACTION','TRIGGER']
};

@@ -27,6 +27,13 @@ // Copyright (C) 2011 Tri Tech Computers Ltd.

var ics = require('./icalendar');
var CalendarObject = require('./base').CalendarObject;
var schema = require('./base').schema;
var properties = require('./base').properties;
var format_value = exports.format_value = types.format_value;
var iCalendar = require('./icalendar').iCalendar;
var parse_value = types.parse_value;
var format_value = types.format_value;
var ParseError = exports.ParseError = function() {

@@ -57,6 +64,6 @@ Error.apply(this, arguments);

if(element == 'BEGIN') {
var factory = (ics.schema[value] || {}).factory;
var factory = (schema[value] || {}).factory;
var child = factory !== undefined
? new factory(cal)
: new ics.CalendarObject(cal, value);
: new CalendarObject(cal, value);

@@ -76,3 +83,3 @@ return parse_component(child, function() {

else {
value = ics.parse_value((ics.properties[element] || {}).type,
value = parse_value((properties[element] || {}).type,
value, parameters, cal);

@@ -91,3 +98,3 @@ component.addProperty(element, value, parameters);

data = data.split(/\r?\n/);
var calendar = new ics.iCalendar(true);
var calendar = new iCalendar(true);
if(timezone) {

@@ -97,3 +104,3 @@ if(typeof timezone === 'string')

if(timezone instanceof ics.iCalendar) {
if(timezone instanceof iCalendar) {
var tzs = timezone.getComponents('VTIMEZONE');

@@ -100,0 +107,0 @@ for(var i=0; i<tzs.length; ++i)

@@ -1,956 +0,461 @@

/**
* This class is used to determine new for a recurring event, when the next
* events occur.
*
* This iterator may loop infinitely in the future, therefore it is important
* that if you use this class, you set hard limits for the amount of iterations
* you want to handle.
*
* Note that currently there is not full support for the entire iCalendar
* specification, as it's very complex and contains a lot of permutations
* that's not yet used very often in software.
*
* For the focus has been on features as they actually appear in Calendaring
* software, but this may well get expanded as needed / on demand
*
* The following RRULE properties are supported
* * UNTIL
* * INTERVAL
* * FREQ=DAILY
* * BYDAY
* * FREQ=WEEKLY
* * BYDAY
* * WKST
* * FREQ=MONTHLY
* * BYMONTHDAY
* * BYDAY
* * BYSETPOS
* * FREQ=YEARLY
* * BYMONTH
* * BYMONTHDAY (only if BYMONTH is also set)
* * BYDAY (only if BYMONTH is also set)
*
* Anything beyond this is 'undefined', which means that it may get ignored, or
* you may get unexpected results. The effect is that in some applications the
* specified recurrence may look incorrect, or is missing.
*
* @package Sabre
* @subpackage VObject
* @copyright Copyright (C) 2007-2012 Rooftop Solutions. All rights reserved.
* @author Evert Pot (http://www.rooftopsolutions.nl/)
* @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
*/
var RRuleIterator = exports.RRuleIterator = function() {
if (is_null($uid)) {
if ($vcal->name === 'VCALENDAR') {
throw new InvalidArgumentException('If you pass a VCALENDAR object, you must pass a uid argument as well');
}
$components = array($vcal);
$uid = (string)$vcal->uid;
} else {
$components = $vcal->select('VEVENT');
}
foreach($components as $component) {
if ((string)$component->uid == $uid) {
if (isset($component->{'RECURRENCE-ID'})) {
$this->overriddenEvents[$component->DTSTART->getDateTime()->getTimeStamp()] = $component;
$this->exceptionDates[] = $component->{'RECURRENCE-ID'}->getDateTime();
} else {
$this->baseEvent = $component;
}
}
}
if (!$this->baseEvent) {
throw new InvalidArgumentException('Could not find a base event with uid: ' . $uid);
}
// Copyright (C) 2011 Tri Tech Computers Ltd.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of
// this software and associated documentation files (the "Software"), to deal in
// the Software without restriction, including without limitation the rights to
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
// of the Software, and to permit persons to whom the Software is furnished to do
// so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
//
//
// NB: All calculations here happen using the UTC portion of a datetime object
// as if it were the local time. This is done to reuse the TZ-agnostic date
// calculations provided to us. Without this, performing date calculations
// across local DST boundaries would yield surprising results.
//
$this->startDate = clone $this->baseEvent->DTSTART->getDateTime();
$this->endDate = null;
if (isset($this->baseEvent->DTEND)) {
$this->endDate = clone $this->baseEvent->DTEND->getDateTime();
} else {
$this->endDate = clone $this->startDate;
if (isset($this->baseEvent->DURATION)) {
$this->endDate->add(Sabre_VObject_DateTimeParser::parse($this->baseEvent->DURATION->value));
}
}
$this->currentDate = clone $this->startDate;
var types = require('./types');
$rrule = (string)$this->baseEvent->RRULE;
var SUPPORTED_PARTS = ['FREQ','INTERVAL','COUNT','UNTIL','BYDAY','BYMONTH','BYMONTHDAY'];
var WKDAYS = ['SU','MO','TU','WE','TH','FR','SA'];
$parts = explode(';', $rrule);
function to_utc_date(dt) {
if(Array.isArray(dt)) {
dt = dt.slice(0); // Make a copy...
dt[1]--; // Fixup month for Date.UTC()
}
else
dt = [dt.getFullYear(), dt.getMonth(), dt.getDate(),
dt.getHours(), dt.getMinutes(), dt.getSeconds(), dt.getMilliseconds()];
foreach($parts as $part) {
return new Date(Date.UTC.apply(null, dt));
}
list($key, $value) = explode('=', $part, 2);
function from_utc_date(dt) {
return new Date(dt.getUTCFullYear(), dt.getUTCMonth(), dt.getUTCDate(),
dt.getUTCHours(), dt.getUTCMinutes(), dt.getUTCSeconds(), dt.getUTCMilliseconds());
}
switch(strtoupper($key)) {
// Return only the whole number portion of a number
function trunc(n) {
return n < 0 ? Math.ceil(n) : Math.floor(n);
}
case 'FREQ' :
if (!in_array(
strtolower($value),
array('secondly','minutely','hourly','daily','weekly','monthly','yearly')
)) {
throw new InvalidArgumentException('Unknown value for FREQ=' . strtoupper($value));
// These are more comfy to type...
function y(dt) { return dt.getUTCFullYear(); }
function m(dt) { return dt.getUTCMonth()+1; }
function d(dt) { return dt.getUTCDate(); }
function hr(dt) { return dt.getUTCHours(); }
function min(dt) { return dt.getUTCMinutes(); }
function sec(dt) { return dt.getUTCSeconds(); }
}
$this->frequency = strtolower($value);
break;
function set_y(dt, v) { dt.setUTCFullYear(v); return dt; }
function set_m(dt, v) { dt.setUTCMonth(v-1); return dt; }
function set_d(dt, v) { dt.setUTCDate(v); return dt; }
function set_hr(dt, v) { dt.setUTCHours(v); return dt; }
function set_min(dt, v) { dt.setUTCMinutes(v); return dt; }
function set_sec(dt, v) { dt.setUTCSeconds(v); return dt; }
case 'UNTIL' :
$this->until = Sabre_VObject_DateTimeParser::parse($value);
break;
function add_y(dt, v) { return set_y(dt, y(dt)+v); }
function add_m(dt, v) { return set_m(dt, m(dt)+v); }
function add_d(dt, v) { return set_d(dt, d(dt)+v); }
function add_hr(dt, v) { return set_hr(dt, hr(dt)+v); }
function add_min(dt, v) { return set_min(dt, min(dt)+v); }
function add_sec(dt, v) { return set_sec(dt, sec(dt)+v); }
case 'COUNT' :
$this->count = (int)$value;
break;
// First of the month
function fst(dt) {
return new Date(y(dt), m(dt)-1, 1);
}
case 'INTERVAL' :
$this->interval = (int)$value;
break;
// Day of week (0-6), adjust for the start of week
function wkday(dt) {
return dt.getUTCDay();
}
case 'BYSECOND' :
$this->bySecond = explode(',', $value);
break;
// Return the number of days between dt1 and dt2
function daydiff(dt1, dt2) {
return (dt2-dt1)/(1000*60*60*24);
}
case 'BYMINUTE' :
$this->byMinute = explode(',', $value);
break;
// Week of year
function wk(dt) {
var jan1 = new Date(Date.UTC(y(dt), 0, 1));
return trunc(daydiff(jan1, dt)/7);
}
case 'BYHOUR' :
$this->byHour = explode(',', $value);
break;
// Week of month
function m_wk(dt, wkst) {
return (0 | d(dt)/7) + (d(dt) % 7 === 0 ? 0 : 1);
}
case 'BYDAY' :
$this->byDay = explode(',', strtoupper($value));
break;
case 'BYMONTHDAY' :
$this->byMonthDay = explode(',', $value);
break;
var RRule = exports.RRule = function(rule, start, end) {
this.start = start ? to_utc_date(start) : null;
this.end = end ? to_utc_date(end) : null;
case 'BYYEARDAY' :
$this->byYearDay = explode(',', $value);
break;
if(typeof rule === 'string')
rule = RRule.parse(rule);
case 'BYWEEKNO' :
$this->byWeekNo = explode(',', $value);
break;
this.rule = {};
for(var i in (rule||{})) {
if(SUPPORTED_PARTS.indexOf(i) == -1)
throw new Error(i+" is not currently supported!");
case 'BYMONTH' :
$this->byMonth = explode(',', $value);
break;
case 'BYSETPOS' :
$this->bySetPos = explode(',', $value);
break;
case 'WKST' :
$this->weekStart = strtoupper($value);
break;
}
this.rule[i] = RULE_PARTS[i]
? RULE_PARTS[i].parse(rule[i])
: rule[i];
}
}
// Parsing exception dates
if (isset($this->baseEvent->EXDATE)) {
foreach($this->baseEvent->EXDATE as $exDate) {
foreach(explode(',', (string)$exDate) as $exceptionDate) {
$this->exceptionDates[] =
Sabre_VObject_DateTimeParser::parse($exceptionDate, $this->startDate->getTimeZone());
}
}
RRule.parse = function(value) {
var parts = value.split(/=|;/);
var rrule = {};
for(var i=0; i<parts.length; i+=2) {
rrule[parts[i]] = parts[i+1];
}
return rrule;
}
RRule.prototype.setFrequency = function(freq) {
this.rule.FREQ = freq;
}
class Sabre_VObject_RecurrenceIterator implements Iterator {
RRule.prototype.valueOf = function() { return this.rule; }
/**
* The initial event date
*
* @var DateTime
*/
public $startDate;
RRule.prototype.toString = function() {
// FREQ comes first, as per spec
var out = [ 'FREQ='+this.rule.FREQ ];
for(var k in this.rule) {
if(k=='FREQ') continue;
/**
* The end-date of the initial event
*
* @var DateTime
*/
public $endDate;
/**
* The 'current' recurrence.
*
* This will be increased for every iteration.
*
* @var DateTime
*/
public $currentDate;
/**
* List of dates that are excluded from the rules.
*
* This list contains both the items that are specified in the EXDATE
* property, as well as the ones that have been overridden by other events
* and RECURRENCE-ID.
*
* @var array
*/
public $exceptionDates = array();
/**
* Base event
*
* @var Sabre_VObject_Component_VEvent
*/
public $baseEvent;
/**
* list of events that are 'overridden'.
*
* This is an array of Sabre_VObject_Component_VEvent objects.
*
* @var array
*/
public $overriddenEvents = array();
/**
* Frequency is one of: secondly, minutely, hourly, daily, weekly, monthly,
* yearly.
*
* @var string
*/
public $frequency;
/**
* The last instance of this recurrence, inclusively
*
* @var DateTime|null
*/
public $until;
/**
* The number of recurrences, or 'null' if infinitely recurring.
*
* @var int
*/
public $count;
/**
* The interval.
*
* If for example frequency is set to daily, interval = 2 would mean every
* 2 days.
*
* @var int
*/
public $interval = 1;
/**
* Which seconds to recur.
*
* This is an array of integers (between 0 and 60)
*
* @var array
*/
public $bySecond;
/**
* Which minutes to recur
*
* This is an array of integers (between 0 and 59)
*
* @var array
*/
public $byMinute;
/**
* Which hours to recur
*
* This is an array of integers (between 0 and 23)
*
* @var array
*/
public $byHour;
/**
* Which weekdays to recur.
*
* This is an array of weekdays
*
* This may also be preceeded by a positive or negative integer. If present,
* this indicates the nth occurrence of a specific day within the monthly or
* yearly rrule. For instance, -2TU indicates the second-last tuesday of
* the month, or year.
*
* @var array
*/
public $byDay;
/**
* Which days of the month to recur
*
* This is an array of days of the months (1-31). The value can also be
* negative. -5 for instance means the 5th last day of the month.
*
* @var array
*/
public $byMonthDay;
/**
* Which days of the year to recur.
*
* This is an array with days of the year (1 to 366). The values can also
* be negative. For instance, -1 will always represent the last day of the
* year. (December 31st).
*
* @var array
*/
public $byYearDay;
/**
* Which week numbers to recur.
*
* This is an array of integers from 1 to 53. The values can also be
* negative. -1 will always refer to the last week of the year.
*
* @var array
*/
public $byWeekNo;
/**
* Which months to recur
*
* This is an array of integers from 1 to 12.
*
* @var array
*/
public $byMonth;
/**
* Which items in an existing st to recur.
*
* These numbers work together with an existing by* rule. It specifies
* exactly which items of the existing by-rule to filter.
*
* Valid values are 1 to 366 and -1 to -366. As an example, this can be
* used to recur the last workday of the month.
*
* This would be done by setting frequency to 'monthly', byDay to
* 'MO,TU,WE,TH,FR' and bySetPos to -1.
*
* @var array
*/
public $bySetPos;
/**
* When a week starts
*
* @var string
*/
public $weekStart = 'MO';
/**
* The current item in the list
*
* @var int
*/
public $counter = 0;
/**
* Simple mapping from iCalendar day names to day numbers
*
* @var array
*/
private $dayMap = array(
'SU' => 0,
'MO' => 1,
'TU' => 2,
'WE' => 3,
'TH' => 4,
'FR' => 5,
'SA' => 6,
);
/**
* Mappings between the day number and english day name.
*
* @var array
*/
private $dayNames = array(
0 => 'Sunday',
1 => 'Monday',
2 => 'Tuesday',
3 => 'Wednesday',
4 => 'Thursday',
5 => 'Friday',
6 => 'Saturday',
);
/**
* If the current iteration of the event is an overriden event, this
* property will hold the VObject
*
* @var Sabre_Component_VObject
*/
private $currentOverriddenEvent;
/**
* This property may contain the date of the next not-overridden event.
* This date is calculated sometimes a bit early, before overridden events
* are evaluated.
*
* @var DateTime
*/
private $nextDate;
/**
* Creates the iterator
*
* You should pass a VCALENDAR component, as well as the UID of the event
* we're going to traverse.
*
* @param Sabre_VObject_Component $comp
*/
public function __construct(Sabre_VObject_Component $vcal, $uid=null) {
out.push(k+'='+((RULE_PARTS[k] || {}).format
? RULE_PARTS[k].format(this.rule[k])
: this.rule[k]));
}
return out.join(';');
}
/**
* Returns the current item in the list
*
* @return DateTime
*/
public function current() {
// Return the next occurrence after dt
RRule.prototype.next = function(after) {
after = after && to_utc_date(after);
if (!$this->valid()) return null;
return clone $this->currentDate;
// Events don't occur before the start or after the end...
if(!after || after < this.start)
return from_utc_date(this.start);
if(this.end && after > this.end) return null;
}
var freq = FREQ[this.rule.FREQ];
if(!freq)
throw new Error(this.rule.FREQ+' recurrence is not supported');
/**
* This method returns the startdate for the current iteration of the
* event.
*
* @return DateTime
*/
public function getDtStart() {
var next = freq.next(this.rule, this.start, after);
return clone $this->currentDate;
// Date is off the end of the spectrum...
if(this.end && next > this.end)
return null;
}
/**
* This method returns the enddate for the current iteration of the
* event.
*
* @return DateTime
*/
public function getDtEnd() {
$dtEnd = clone $this->currentDate;
$dtEnd->add( $this->startDate->diff( $this->endDate ) );
return clone $dtEnd;
}
/**
* Returns a VEVENT object with the updated start and end date.
*
* Any recurrence information is removed, and this function may return an
* 'overridden' event instead.
*
* This method always returns a cloned instance.
*
* @return void
*/
public function getEventObject() {
if ($this->currentOverriddenEvent) {
return clone $this->currentOverriddenEvent;
if(this.rule.COUNT && this.count_end !== null) {
if(this.count_end === undefined) {
// Don't check this while we're trying to compute it...
this.count_end = null;
this.count_end = this.nextOccurences(this.rule.COUNT).pop();
}
$event = clone $this->baseEvent;
unset($event->RRULE);
unset($event->EXDATE);
unset($event->RDATE);
unset($event->EXRULE);
$event->DTSTART->setDateTime($this->currentDate, $event->DTSTART->getDateType());
if (isset($event->DTEND)) {
$event->DTEND->setDateTime($this->getDtEnd(), $event->DTSTART->getDateType());
}
if ($this->counter > 0) {
$event->{'RECURRENCE-ID'} = (string)$event->DTSTART;
}
return $event;
if(next > this.count_end)
return null;
}
/**
* Returns the current item number
*
* @return int
*/
public function key() {
if(this.rule.UNTIL && next > this.rule.UNTIL)
return null;
return $this->counter;
return from_utc_date(next);
}
RRule.prototype.nextOccurences = function(after, count) {
if(arguments.length === 1) {
count = after;
after = undefined;
}
/**
* Whether or not there is a 'next item'
*
* @return bool
*/
public function valid() {
if (!is_null($this->count)) {
return $this->counter < $this->count;
}
if (!is_null($this->until)) {
return $this->currentDate <= $this->until;
}
return true;
var arr = [];
while(count-- && after !== null) {
after = this.next(after);
if(after)
arr.push(after);
}
return arr;
}
/**
* Resets the iterator
*
* @return void
*/
public function rewind() {
$this->currentDate = clone $this->startDate;
$this->counter = 0;
var RULE_PARTS = {
INTERVAL: {
parse: function(v) { return parseInt(v,10); }
},
UNTIL: {
parse: function(v) { return types.parse_value('DATE-TIME', v); },
format: function(v) { return types.format_value('DATE-TIME', v); }
},
FREQ: {
parse: function(v) { return v; },
},
BYMONTH: {
parse: function(v) {
if(typeof v === 'number') return [v];
}
return v.split(',').map(function(mo) {
return parseInt(mo,10);
});
},
format: function(v) {
return v.join(',');
}
},
BYDAY: { // 2TH (second thursday) -> [2,4]
parse: function(v) {
var days = v.split(',').map(function(day) {
var m = day.match(/([+-]?\d)?(SU|MO|TU|WE|TH|FR|SA)/);
return [parseInt(m[1],10)||0, WKDAYS.indexOf(m[2])];
});
/**
* This method allows you to quickly go to the next occurrence after the
* specified date.
*
* Note that this checks the current 'endDate', not the 'stardDate'. This
* means that if you forward to January 1st, the iterator will stop at the
* first event that ends *after* January 1st.
*
* @param DateTime $dt
* @return void
*/
public function fastForward(DateTime $dt) {
days.sort(function(d1, d2) {
// Sort by week, day of week
if(d1[0] == d2[0])
return d1[1] - d2[1];
else
return d1[0] - d2[0];
});
while($this->valid() && $this->getDTEnd() < $dt) {
$this->next();
return days;
},
format: function(v) {
return v.map(function(day) {
return (day[0] || '')+WKDAYS[day[1]];
}).join(',');
}
}
};
/**
* Goes on to the next iteration
*
* @return void
*/
public function next() {
// These parts use the same format...
RULE_PARTS['BYMONTHDAY'] = RULE_PARTS['BYMONTH'];
RULE_PARTS['COUNT'] = RULE_PARTS['INTERVAL'];
$previousStamp = $this->currentDate->getTimeStamp();
var FREQ = {
DAILY: {
next: function(rule, start, after) {
var next = new Date(after);
var interval = rule.INTERVAL || 1;
while(true) {
// Adjust for interval...
var mod_days = daydiff(next, start) % interval;
if(mod_days)
add_d(next, interval - mod_days);
$this->currentOverriddenEvent = null;
for(var i=0; i<2; ++i) {
next = byday(rule.BYDAY, next);
set_hr(next, hr(start));
set_min(next, min(start));
set_sec(next, sec(start));
// If we have a next date 'stored', we use that
if ($this->nextDate) {
$this->currentDate = $this->nextDate;
$currentStamp = $this->currentDate->getTimeStamp();
$this->nextDate = null;
} else {
if(after.valueOf() != next.valueOf())
break;
// Otherwise, we calculate it
switch($this->frequency) {
case 'daily' :
$this->nextDaily();
break;
case 'weekly' :
$this->nextWeekly();
break;
case 'monthly' :
$this->nextMonthly();
break;
case 'yearly' :
$this->nextYearly();
break;
}
$currentStamp = $this->currentDate->getTimeStamp();
// Checking exception dates
foreach($this->exceptionDates as $exceptionDate) {
if ($this->currentDate == $exceptionDate) {
continue 2;
}
}
add_d(next, interval);
}
// Checking overriden events
foreach($this->overriddenEvents as $index=>$event) {
if ($index > $previousStamp && $index < $currentStamp) {
// We're moving the 'next date' aside, for later use.
$this->nextDate = clone $this->currentDate;
$this->currentDate = $event->DTSTART->getDateTime();
$this->currentOverriddenEvent = $event;
break;
}
}
break;
return next;
}
},
WEEKLY: {
next: function(rule, start, after) {
var next = new Date(after);
var interval = rule.INTERVAL || 1;
$this->counter++;
// Adjust for interval...
var days = daydiff(start, next);
if(days) add_d(next, 7 - (days % 7));
}
var mod_weeks = Math.floor(daydiff(next, start) / 7) % interval;
if(mod_weeks)
add_d(next, (interval - mod_weeks) * 7);
/**
* Does the processing for advancing the iterator for daily frequency.
*
* @return void
*/
protected function nextDaily() {
while(true) {
next = byday(rule.BYDAY, next);
set_hr(next, hr(start));
set_min(next, min(start));
set_sec(next, sec(start));
if (!$this->byDay) {
$this->currentDate->modify('+' . $this->interval . ' days');
return;
}
if(after.valueOf() != next.valueOf()
&& check_bymonth(rule.BYMONTH, next))
break;
$recurrenceDays = array();
foreach($this->byDay as $byDay) {
add_d(next, interval * 7);
}
// The day may be preceeded with a positive (+n) or
// negative (-n) integer. However, this does not make
// sense in 'weekly' so we ignore it here.
$recurrenceDays[] = $this->dayMap[substr($byDay,-2)];
return next;
}
},
MONTHLY: {
next: function(rule, start, after) {
var next = new Date(after);
var interval = rule.INTERVAL || 1;
do {
// Adjust interval to be correct
var delta = (m(next) - m(start)) + (y(next) - y(start)) * 12;
if(delta % interval)
add_m(next, interval - (delta % interval));
$this->currentDate->modify('+' . $this->interval . ' days');
for(var i=0; i<2; ++i) {
next = byday(rule.BYDAY, next);
next = bymonthday(rule.BYMONTHDAY, next);
set_hr(next, hr(start));
set_min(next, min(start));
set_sec(next, sec(start));
// Current day of the week
$currentDay = $this->currentDate->format('w');
if(after.valueOf() != next.valueOf())
break;
} while (!in_array($currentDay, $recurrenceDays));
}
/**
* Does the processing for advancing the iterator for weekly frequency.
*
* @return void
*/
protected function nextWeekly() {
if (!$this->byDay) {
$this->currentDate->modify('+' . $this->interval . ' weeks');
return;
}
$recurrenceDays = array();
foreach($this->byDay as $byDay) {
// The day may be preceeded with a positive (+n) or
// negative (-n) integer. However, this does not make
// sense in 'weekly' so we ignore it here.
$recurrenceDays[] = $this->dayMap[substr($byDay,-2)];
}
// Current day of the week
$currentDay = $this->currentDate->format('w');
// First day of the week:
$firstDay = $this->dayMap[$this->weekStart];
// Increasing the 'current day' until we find our next
// occurrence.
while(true) {
$currentDay++;
if ($currentDay>6) {
$currentDay = 0;
add_m(next, interval);
}
// We need to roll over to the next week
if ($currentDay === $firstDay) {
$this->currentDate->modify('+' . $this->interval . ' weeks');
$this->currentDate->modify('last ' . $this->dayNames[$this->dayMap[$this->weekStart]]);
}
// We have a match
if (in_array($currentDay ,$recurrenceDays)) {
$this->currentDate->modify($this->dayNames[$currentDay]);
break;
}
return next;
}
},
YEARLY: {
next: function(rule, start, after) {
// Occurs every N years...
var next = new Date(after);
var interval = rule.INTERVAL || 1;
}
var mod_year = (y(after) - y(start)) % interval;
if(mod_year)
// We're not in a valid year, move to the next valid year
add_y(next, interval - mod_year);
/**
* Does the processing for advancing the iterator for monthly frequency.
*
* @return void
*/
protected function nextMonthly() {
$currentDayOfMonth = $this->currentDate->format('j');
if (!$this->byMonthDay && !$this->byDay) {
for(var i=0; i<2; ++i) {
next = bymonth(rule.BYMONTH || m(start), next, i);
next = bymonthday(rule.BYMONTHDAY, next);
next = byday(rule.BYDAY, next, i);
// If the current day is higher than the 28th, rollover can
// occur to the next month. We Must skip these invalid
// entries.
if ($currentDayOfMonth < 29) {
$this->currentDate->modify('+' . $this->interval . ' months');
} else {
$increase = 0;
do {
$increase++;
$tempDate = clone $this->currentDate;
$tempDate->modify('+ ' . ($this->interval*$increase) . ' months');
} while ($tempDate->format('j') != $currentDayOfMonth);
$this->currentDate = $tempDate;
}
return;
}
// TODO: Add actual byhour/minute/second methods
set_hr(next, hr(start));
set_min(next, min(start));
set_sec(next, sec(start));
while(true) {
// Don't loop back again if we found a new date
if(after.valueOf() != next.valueOf())
break;
$occurrences = $this->getMonthlyOccurrences();
foreach($occurrences as $occurrence) {
// The first occurrence thats higher than the current
// day of the month wins.
if ($occurrence > $currentDayOfMonth) {
break 2;
}
set_d(set_m(add_y(next, interval), 1), 1);
}
// If we made it all the way here, it means there were no
// valid occurrences, and we need to advance to the next
// month.
$this->currentDate->modify('first day of this month');
$this->currentDate->modify('+ ' . $this->interval . ' months');
// This goes to 0 because we need to start counting at hte
// beginning.
$currentDayOfMonth = 0;
return next;
}
$this->currentDate->setDate($this->currentDate->format('Y'), $this->currentDate->format('n'), $occurrence);
}
};
/**
* Does the processing for advancing the iterator for yearly frequency.
*
* @return void
*/
protected function nextYearly() {
function sort_dates(dateary) {
return dateary.sort(function(dt1, dt2) {
if(dt1 === null && dt2 === null) return 0;
if(dt1 === null) return 1;
if(dt2 === null) return -1;
if (!$this->byMonth) {
$this->currentDate->modify('+' . $this->interval . ' years');
return;
}
return dt1.valueOf() - dt2.valueOf();
});
}
$currentMonth = $this->currentDate->format('n');
$currentYear = $this->currentDate->format('Y');
$currentDayOfMonth = $this->currentDate->format('j');
// Check that a particular date is within the limits
// designated by the BYMONTH rule
function check_bymonth(rules, dt) {
if(!rules || !rules.length) return true;
return rules.indexOf(m(dt)) !== -1;
}
// If we got a byDay or getMonthDay filter, we must first expand
// further.
if ($this->byDay || $this->byMonthDay) {
// Advance to the next month that satisfies the rule...
function bymonth(rules, dt) {
if(!rules || !rules.length) return dt;
while(true) {
var candidates = rules.map(function(rule) {
var delta = rule-m(dt);
if(delta < 0) delta += 12;
$occurrences = $this->getMonthlyOccurrences();
var newdt = add_m(new Date(dt), delta);
set_d(newdt, 1);
return newdt;
});
var newdt = sort_dates(candidates).shift();
return newdt || dt;
}
foreach($occurrences as $occurrence) {
// The first occurrence that's higher than the current
// day of the month wins.
if ($occurrence > $currentDayOfMonth) {
break 2;
}
function bymonthday(rules, dt) {
if(!rules || !rules.length) return dt;
}
var candidates = rules.map(function(rule) {
var delta = rule-m(dt);
if(delta < 0) delta += 12;
// If we made it here, it means we need to advance to
// the next month or year.
$currentDayOfMonth = 1;
do {
var newdt = set_d(new Date(dt), rule);
return (newdt < dt ? null : newdt);
});
$currentMonth++;
if ($currentMonth>12) {
$currentYear+=$this->interval;
$currentMonth = 1;
}
} while (!in_array($currentMonth, $this->byMonth));
var newdt = sort_dates(candidates).shift();
return newdt || dt;
}
$this->currentDate->setDate($currentYear, $currentMonth, $currentDayOfMonth);
}
// Advance to the next day that satisfies the byday rule...
function byday(rules, dt) {
if(!rules || !rules.length) return dt;
// If we made it here, it means we got a valid occurrence
$this->currentDate->setDate($currentYear, $currentMonth, $occurrence);
return;
// Generate a list of candiDATES. (HA!)
var candidates = rules.map(function(rule) {
// Align on the correct day of the week...
var days = rule[1]-wkday(dt);
if(days < 0) days += 7;
var newdt = add_d(new Date(dt), days);
} else {
if(rule[0] > 0) {
var wk = 0 | (d(newdt) / 7) + 1;
if(wk > rule[0]) return null;
// no byDay or byMonthDay, so we can just loop through the
// months.
do {
$currentMonth++;
if ($currentMonth>12) {
$currentYear+=$this->interval;
$currentMonth = 1;
}
} while (!in_array($currentMonth, $this->byMonth));
$this->currentDate->setDate($currentYear, $currentMonth, $currentDayOfMonth);
return;
add_d(newdt, (rule[0] - wk) * 7);
}
}
/**
* Returns all the occurrences for a monthly frequency with a 'byDay' or
* 'byMonthDay' expansion for the current month.
*
* The returned list is an array of integers with the day of month (1-31).
*
* @return array
*/
protected function getMonthlyOccurrences() {
$startDate = clone $this->currentDate;
$byDayResults = array();
// Our strategy is to simply go through the byDays, advance the date to
// that point and add it to the results.
if ($this->byDay) foreach($this->byDay as $day) {
$dayName = $this->dayNames[$this->dayMap[substr($day,-2)]];
// Dayname will be something like 'wednesday'. Now we need to find
// all wednesdays in this month.
$dayHits = array();
$checkDate = clone $startDate;
$checkDate->modify('first day of this month');
$checkDate->modify($dayName);
do {
$dayHits[] = $checkDate->format('j');
$checkDate->modify('next ' . $dayName);
} while ($checkDate->format('n') === $startDate->format('n'));
// So now we have 'all wednesdays' for month. It is however
// possible that the user only really wanted the 1st, 2nd or last
// wednesday.
if (strlen($day)>2) {
$offset = (int)substr($day,0,-2);
if ($offset>0) {
$byDayResults[] = $dayHits[$offset-1];
} else {
// if it was negative we count from the end of the array
$byDayResults[] = $dayHits[count($dayHits) + $offset];
}
} else {
// There was no counter (first, second, last wednesdays), so we
// just need to add the all to the list).
$byDayResults = array_merge($byDayResults, $dayHits);
else if(rule[0] < 0) {
// Find all the matching days in the month...
var dt2 = new Date(newdt);
var days = [];
while(m(dt2) === m(newdt)) {
days.push(d(dt2));
add_d(dt2, 7);
}
// Then grab the nth from the end...
set_d(newdt, days.reverse()[(-rule[0])-1]);
}
$byMonthDayResults = array();
if ($this->byMonthDay) foreach($this->byMonthDay as $monthDay) {
// Don't look outside the current month...
if(m(newdt) !== m(dt)) return null;
// Removing values that are out of range for this month
if ($monthDay > $startDate->format('t') ||
$monthDay < 0-$startDate->format('t')) {
continue;
}
if ($monthDay>0) {
$byMonthDayResults[] = $monthDay;
} else {
// Negative values
$byMonthDayResults[] = $startDate->format('t') + 1 + $monthDay;
}
}
return newdt;
});
// If there was just byDay or just byMonthDay, they just specify our
// (almost) final list. If both were provided, then byDay limits the
// list.
if ($this->byMonthDay && $this->byDay) {
$result = array_intersect($byMonthDayResults, $byDayResults);
} elseif ($this->byMonthDay) {
$result = $byMonthDayResults;
} else {
$result = $byDayResults;
}
$result = array_unique($result);
sort($result, SORT_NUMERIC);
// The last thing that needs checking is the BYSETPOS. If it's set, it
// means only certain items in the set survive the filter.
if (!$this->bySetPos) {
return $result;
}
$filteredResult = array();
foreach($this->bySetPos as $setPos) {
if ($setPos<0) {
$setPos = count($result)-($setPos+1);
}
if (isset($result[$setPos-1])) {
$filteredResult[] = $result[$setPos-1];
}
}
sort($filteredResult, SORT_NUMERIC);
return $filteredResult;
}
// Select the date occurring next...
var newdt = sort_dates(candidates).shift();
return newdt || dt;
}

@@ -23,3 +23,3 @@ // Copyright (C) 2011 Tri Tech Computers Ltd.

var RRule = require('./recur').RRule;
var RRule = require('./rrule').RRule;

@@ -26,0 +26,0 @@ function pad(n,d) {

{
"name": "icalendar",
"version": "0.4.1",
"version": "0.5.0",
"author": "James Emerton <james@tri-tech.com>",

@@ -13,3 +13,3 @@ "description": "RFC5545 iCalendar parser/generator",

},
"main": "lib/icalendar",
"main": "lib/index",
"devDependencies": {

@@ -16,0 +16,0 @@ "jasmine-node": ">=1.0.13"

@@ -6,3 +6,3 @@ // Test search

var icalendar = require('../lib/icalendar');
var icalendar = require('../lib');

@@ -94,7 +94,18 @@ // NB: Ported to jasmine from expresso, hence the strange layout

assert.equal(-400, tz.getOffsetForDate(new Date(2011,6,2)));
// These are easy...
expect(tz.getOffsetForDate(new Date(2011,6,2))).toEqual(-400);
expect(tz.getOffsetForDate(new Date(2011,1,2))).toEqual(-500);
// Do we handle transitions correctly?
// NB: These come in as an array because Date objects can't
// represent a time that doesn't actually exist
expect(tz.getOffsetForDate([2011,3,13,1,59,59])).toEqual(-500);
expect(tz.getOffsetForDate([2011,3,13,2,0,0])).toEqual(-400);
expect(tz.getOffsetForDate([2011,11,6,1,59,59])).toEqual(-400);
expect(tz.getOffsetForDate([2011,11,6,2,0,0])).toEqual(-500);
});
it('creates calendar clones', function() {
var cal = icalendar.iCalendar.parse(
var cal = icalendar.parse_calendar(
'BEGIN:VCALENDAR\r\n'+

@@ -101,0 +112,0 @@ 'PRODID:-//Bobs Software Emporium//NONSGML Bobs Calendar//EN\r\n'+

@@ -6,7 +6,7 @@ // Test search

var icalendar = require('../lib/icalendar');
var parse_calendar = require('../lib/parser').parse_calendar;
describe("iCalendar.parse", function() {
it('parses data correctly', function() {
var cal = icalendar.iCalendar.parse(
var cal = parse_calendar(
'BEGIN:VCALENDAR\r\n'+

@@ -36,3 +36,3 @@ 'PRODID:-//Bobs Software Emporium//NONSGML Bobs Calendar//EN\r\n'+

it('parses large collections', function() {
var cal = icalendar.iCalendar.parse(
var cal = parse_calendar(
fs.readFileSync(__dirname+'/icalendar-test.ics', 'utf8'));

@@ -60,3 +60,3 @@

// Evolution doesn't seem to provide very consistent line endings
var cal = icalendar.iCalendar.parse(
var cal = parse_calendar(
fs.readFileSync(__dirname+'/evolution.ics', 'utf8'));

@@ -66,3 +66,3 @@ });

it('parsing', function() {
var cal = icalendar.iCalendar.parse(
var cal = parse_calendar(
'BEGIN:VCALENDAR\r\n'+

@@ -92,3 +92,3 @@ 'PRODID:-//Bobs Software Emporium//NONSGML Bobs Calendar//EN\r\n'+

it('parse torture test', function() {
var cal = icalendar.iCalendar.parse(
var cal = parse_calendar(
fs.readFileSync(__dirname+'/icalendar-test.ics', 'utf8'));

@@ -115,3 +115,3 @@

it('uses timezone data parameter when parsing', function() {
var cal = icalendar.parse_calendar(
var cal = parse_calendar(
'BEGIN:VCALENDAR\r\n'+

@@ -118,0 +118,0 @@ 'PRODID:-//Google Inc//Google Calendar 70.9054//EN\r\n'+

var RRule = require('../lib/icalendar').RRule;
var RRule = require('../lib/rrule').RRule;

@@ -4,0 +4,0 @@ describe("RRule", function() {

var icalendar = require('../lib/icalendar');
var icalendar = require('../lib');
var assert = require('assert');

@@ -4,0 +4,0 @@

var icalendar = require('../lib/icalendar');
var icalendar = require('../lib');

@@ -4,0 +4,0 @@ describe('VEvent objects', function() {

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc