--- a/calendar/base/modules/ical.js
+++ b/calendar/base/modules/ical.js
@@ -4,17 +4,17 @@
* Portions Copyright (C) Philipp Kewisch, 2011-2012 */
/**
* This is ical.js from <https://github.com/mozilla-comm/ical.js>.
*
* If you would like to change anything in ical.js, it is required to do so
* upstream first.
*
- * Current ical.js git revision: b2112a5eb6fe2c311ee945150b2a267213c1f18c
+ * Current ical.js git revision: 9d3ee67d1ce9e0bb960e73a45e2dd434360cbd8d
*/
var EXPORTED_SYMBOLS = ["ICAL", "unwrap", "unwrapSetter", "unwrapSingle", "wrapGetter"];
function wrapGetter(type, val) {
return val ? new type(val) : null;
}
@@ -38,17 +38,17 @@ function unwrapSingle(type, val) {
}
}
// -- start ical.js --
if (typeof(ICAL) === 'undefined')
(typeof(window) !== 'undefined') ? this.ICAL = {} : ICAL = {};
-ICAL.foldLength = 74;
+ICAL.foldLength = 75;
ICAL.newLineChar = '\r\n';
ICAL.debug = false;
/**
* Helper functions used in various places within ical.js
*/
ICAL.helpers = {
initState: function initState(aLine, aLineNr) {
@@ -1231,66 +1231,74 @@ ICAL.parse = (function() {
return name.toLowerCase();
}
parser._handleContentLine = function(line, state) {
// break up the parts of the line
var valuePos = line.indexOf(VALUE_DELIMITER);
var paramPos = line.indexOf(PARAM_DELIMITER);
- var nextPos = 0;
+ var lastParamIndex;
+ var lastValuePos;
+
// name of property or begin/end
var name;
var value;
- var params;
+ // params is only overridden if paramPos !== -1.
+ // we can't do params = params || {} later on
+ // because it sacrifices ops.
+ var params = {};
/**
* Different property cases
*
*
* 1. RRULE:FREQ=foo
* // FREQ= is not a param but the value
*
* 2. ATTENDEE;ROLE=REQ-PARTICIPANT;
* // ROLE= is a param because : has not happened yet
*/
+ // when the parameter delimiter is after the
+ // value delimiter then its not a parameter.
if ((paramPos !== -1 && valuePos !== -1)) {
// when the parameter delimiter is after the
// value delimiter then its not a parameter.
if (paramPos > valuePos) {
paramPos = -1;
}
}
+ var parsedParams;
if (paramPos !== -1) {
- // when there are parameters (ATTENDEE;RSVP=TRUE;)
- name = parser._formatName(line.substr(0, paramPos));
- params = parser._parseParameters(line, paramPos);
- if (valuePos !== -1) {
- value = line.substr(valuePos + 1);
+ name = line.substring(0, paramPos).toLowerCase();
+ parsedParams = parser._parseParameters(line.substring(paramPos), 0);
+ params = parsedParams[0];
+ lastParamIndex = parsedParams[1].length + parsedParams[2] + paramPos;
+ if ((lastValuePos =
+ line.substring(lastParamIndex).indexOf(VALUE_DELIMITER)) !== -1) {
+ value = line.substring(lastParamIndex + lastValuePos + 1);
}
} else if (valuePos !== -1) {
// without parmeters (BEGIN:VCAENDAR, CLASS:PUBLIC)
- name = parser._formatName(line.substr(0, valuePos));
- value = line.substr(valuePos + 1);
+ name = line.substring(0, valuePos).toLowerCase();
+ value = line.substring(valuePos + 1);
if (name === 'begin') {
- var newComponent = [parser._formatName(value), [], []];
+ var newComponent = [value.toLowerCase(), [], []];
if (state.stack.length === 1) {
state.component.push(newComponent);
} else {
state.component[2].push(newComponent);
}
state.stack.push(state.component);
state.component = newComponent;
return;
- }
-
- if (name === 'end') {
+ } else if (name === 'end') {
state.component = state.stack.pop();
return;
}
} else {
/**
* Invalid line.
* The rational to throw an error is we will
* never be certain that the rest of the file
@@ -1313,19 +1321,16 @@ ICAL.parse = (function() {
multiValue = propertyDetails.multiValue;
}
if (value && 'detectType' in propertyDetails) {
valueType = propertyDetails.detectType(value);
}
}
- // at this point params is mandatory per jcal spec
- params = params || {};
-
// attempt to determine value
if (!valueType) {
if (!('value' in params)) {
if (propertyDetails) {
valueType = propertyDetails.defaultType;
} else {
valueType = DEFAULT_TYPE;
}
@@ -1380,73 +1385,65 @@ ICAL.parse = (function() {
* @param {Numeric} maxPos position at which values start.
* @return {Object} key/value pairs.
*/
parser._parseParameters = function(line, start) {
var lastParam = start;
var pos = 0;
var delim = PARAM_NAME_DELIMITER;
var result = {};
+ var name;
+ var value;
+ var type;
// find the next '=' sign
// use lastParam and pos to find name
// check if " is used if so get value from "->"
// then increment pos to find next ;
while ((pos !== false) &&
(pos = helpers.unescapedIndexOf(line, delim, pos + 1)) !== -1) {
- var name = line.substr(lastParam + 1, pos - lastParam - 1);
+ name = line.substr(lastParam + 1, pos - lastParam - 1);
var nextChar = line[pos + 1];
- var substrOffset = -2;
-
if (nextChar === '"') {
var valuePos = pos + 2;
pos = helpers.unescapedIndexOf(line, '"', valuePos);
- var value = line.substr(valuePos, pos - valuePos);
+ value = line.substr(valuePos, pos - valuePos);
lastParam = helpers.unescapedIndexOf(line, PARAM_DELIMITER, pos);
} else {
var valuePos = pos + 1;
- substrOffset = -1;
// move to next ";"
var nextPos = helpers.unescapedIndexOf(line, PARAM_DELIMITER, valuePos);
-
if (nextPos === -1) {
// when there is no ";" attempt to locate ":"
nextPos = helpers.unescapedIndexOf(line, VALUE_DELIMITER, valuePos);
- // no more tokens end of the line use .length
+
if (nextPos === -1) {
nextPos = line.length;
- // because we are at the end we don't need to trim
- // the found value of substr offset is zero
- substrOffset = 0;
- } else {
- // next token is the beginning of the value
- // so we must stop looking for the '=' token.
- pos = false;
}
+ pos = false;
} else {
lastParam = nextPos;
}
- var value = line.substr(valuePos, nextPos - valuePos);
- }
-
- var type = DEFAULT_TYPE;
+ value = line.substr(valuePos, nextPos - valuePos);
+ }
if (name in design.param && design.param[name].valueType) {
type = design.param[name].valueType;
- }
-
- result[parser._formatName(name)] = parser._parseValue(value, type);
+ } else {
+ type = DEFAULT_TYPE;
+ }
+
+ result[name.toLowerCase()] = parser._parseValue(value, type);
}
-
- return result;
+ return [result, value, valuePos];
}
/**
* Parse a multi value string
*/
parser._parseMultiValue = function(buffer, delim, type, result) {
var pos = 0;
var lastPos = 0;
@@ -1538,19 +1535,17 @@ ICAL.Component = (function() {
if (typeof(jCal) === 'string') {
// jCal spec (name, properties, components)
jCal = [jCal, [], []];
}
// mostly for legacy reasons.
this.jCal = jCal;
- if (parent) {
- this.parent = parent;
- }
+ this.parent = parent || null;
}
Component.prototype = {
/**
* Hydrated properties are inserted into the _properties array at the same
* position as in the jCal array, so its possible the array contains
* undefined values for unhydrdated properties. To avoid iterating the
* array when checking if all properties have been hydrated, we save the
@@ -1766,16 +1761,20 @@ ICAL.Component = (function() {
}
return null;
},
_removeObjectByIndex: function(jCalIndex, cache, index) {
// remove cached version
if (cache && cache[index]) {
+ var obj = cache[index];
+ if ("parent" in obj) {
+ obj.parent = null;
+ }
cache.splice(index, 1);
}
// remove it from the jCal
this.jCal[jCalIndex].splice(index, 1);
},
_removeObject: function(jCalIndex, cache, nameOrObject) {
@@ -1801,50 +1800,45 @@ ICAL.Component = (function() {
}
return false;
},
_removeAllObjects: function(jCalIndex, cache, name) {
var cached = this[cache];
- if (name) {
- var objects = this.jCal[jCalIndex];
- var i = objects.length - 1;
-
- // descending search required because splice
- // is used and will effect the indices.
- for (; i >= 0; i--) {
- if (objects[i][NAME_INDEX] === name) {
- this._removeObjectByIndex(jCalIndex, cached, i);
- }
+ // Unfortunately we have to run through all children to reset their
+ // parent property.
+ var objects = this.jCal[jCalIndex];
+ var i = objects.length - 1;
+
+ // descending search required because splice
+ // is used and will effect the indices.
+ for (; i >= 0; i--) {
+ if (!name || objects[i][NAME_INDEX] === name) {
+ this._removeObjectByIndex(jCalIndex, cached, i);
}
- } else {
- if (cache in this) {
- // I think its probable that when we remove all
- // of a type we may want to add to it again so it
- // makes sense to reuse the object in that case.
- // For now we remove the contents of the array.
- this[cache].length = 0;
- }
- this.jCal[jCalIndex].length = 0;
}
},
/**
* Adds a single sub component.
*
* @param {ICAL.Component} component to add.
*/
addSubcomponent: function(component) {
if (!this._components) {
this._components = [];
this._hydratedComponentCount = 0;
}
+ if (component.parent) {
+ component.parent.removeSubcomponent(component);
+ }
+
var idx = this.jCal[COMPONENT_INDEX].push(component.jCal);
this._components[idx - 1] = component;
this._hydratedComponentCount++;
component.parent = this;
},
/**
* Removes a single component by name or
@@ -1878,39 +1872,43 @@ ICAL.Component = (function() {
*
* @param {ICAL.Property} property object.
*/
addProperty: function(property) {
if (!(property instanceof ICAL.Property)) {
throw new TypeError('must instance of ICAL.Property');
}
- var idx = this.jCal[PROPERTY_INDEX].push(property.jCal);
- property.component = this;
-
if (!this._properties) {
this._properties = [];
this._hydratedPropertyCount = 0;
}
+
+ if (property.parent) {
+ property.parent.removeProperty(property);
+ }
+
+ var idx = this.jCal[PROPERTY_INDEX].push(property.jCal);
this._properties[idx - 1] = property;
this._hydratedPropertyCount++;
+ property.parent = this;
},
/**
* Helper method to add a property with a value to the component.
*
* @param {String} name property name to add.
* @param {Object} value property value.
*/
addPropertyWithValue: function(name, value) {
- var prop = new ICAL.Property(name, this);
+ var prop = new ICAL.Property(name);
prop.setValue(value);
- this.addProperty(prop, this);
+ this.addProperty(prop);
return prop;
},
/**
* Helper method that will update or create a property
* of the given name and sets its value.
*
@@ -1988,19 +1986,19 @@ ICAL.Property = (function() {
*
* Can also be used to create new properties by passing
* the name of the property (as a String).
*
*
* @param {Array|String} jCal raw jCal representation OR
* the new name of the property (when creating).
*
- * @param {ICAL.Component} [component] parent component.
+ * @param {ICAL.Component} [parent] parent component.
*/
- function Property(jCal, component) {
+ function Property(jCal, parent) {
if (typeof(jCal) === 'string') {
// because we a creating by name we need
// to find the type when creating the property.
var name = jCal;
if (name in design.property) {
var prop = design.property[name];
if ('defaultType' in prop) {
@@ -2011,17 +2009,17 @@ ICAL.Property = (function() {
} else {
var type = design.defaultType;
}
jCal = [name, {}, type];
}
this.jCal = jCal;
- this.component = component;
+ this.parent = parent || null;
this._updateType();
}
Property.prototype = {
get type() {
return this.jCal[TYPE_INDEX];
},
@@ -2226,17 +2224,18 @@ ICAL.Property = (function() {
} else {
for (; i < len; i++) {
this.jCal[VALUE_INDEX + i] = values[i];
}
}
},
/**
- * Sets the current value of the property.
+ * Sets the current value of the property. If this is a multi-value
+ * property, all other values will be removed.
*
* @param {String|Object} value new prop value.
*/
setValue: function(value) {
this.removeAllValues();
if (typeof(value) === 'object' && 'icaltype' in value) {
this.resetType(value.icaltype);
}
@@ -2457,17 +2456,17 @@ ICAL.Binary = (function() {
if (aData && 'start' in aData) {
if (aData.start && !(aData.start instanceof ICAL.Time)) {
throw new TypeError('.start must be an instance of ICAL.Time');
}
this.start = aData.start;
}
- if (aData && ('end' in aData) && ('duration' in aData)) {
+ if (aData && aData.end && aData.duration) {
throw new Error('cannot accept both end and duration');
}
if (aData && 'end' in aData) {
if (aData.end && !(aData.end instanceof ICAL.Time)) {
throw new TypeError('.end must be an instance of ICAL.Time');
}
this.end = aData.end;
@@ -2484,16 +2483,24 @@ ICAL.Binary = (function() {
ICAL.Period.prototype = {
start: null,
end: null,
duration: null,
icalclass: "icalperiod",
icaltype: "period",
+ clone: function() {
+ return ICAL.Period.fromData({
+ start: this.start ? this.start.clone() : null,
+ end: this.end ? this.end.clone() : null,
+ duration: this.duration ? this.duration.clone() : null
+ });
+ },
+
getDuration: function duration() {
if (this.duration) {
return this.duration;
} else {
return this.end.subtractDate(this.start);
}
},
@@ -3288,17 +3295,21 @@ ICAL.TimezoneService = (function() {
time.isDate = false;
this.fromData(data, zone);
};
ICAL.Time.prototype = {
icalclass: "icaltime",
- icaltype: "date-time",
+
+ // is read only strictly defined by isDate
+ get icaltype() {
+ return this.isDate ? 'date' : 'date-time';
+ },
/**
* @type ICAL.Timezone
*/
zone: null,
/**
* Internal uses to indicate that a change has been
@@ -3366,31 +3377,31 @@ ICAL.TimezoneService = (function() {
this.second = aDate.getSeconds();
}
}
return this;
},
fromData: function fromData(aData, aZone) {
for (var key in aData) {
+ // ical type cannot be set
+ if (key === 'icaltype') continue;
this[key] = aData[key];
}
if (aZone) {
this.zone = aZone;
}
if (aData && !("isDate" in aData)) {
this.isDate = !("hour" in aData);
} else if (aData && ("isDate" in aData)) {
this.isDate = aData.isDate;
}
- this.icaltype = (this.isDate ? "date" : "date-time");
-
if (aData && "timezone" in aData) {
var zone = ICAL.TimezoneService.get(
aData.timezone
);
this.zone = zone || ICAL.Timezone.localTimezone;
}
@@ -3779,17 +3790,16 @@ ICAL.TimezoneService = (function() {
_normalize: function icaltime_normalize() {
var isDate = this._time.isDate;
if (this._time.isDate) {
this._time.hour = 0;
this._time.minute = 0;
this._time.second = 0;
}
- this.icaltype = (isDate ? "date" : "date-time");
this.adjust(0, 0, 0, 0);
return this;
},
adjust: function icaltime_adjust(aExtraDays, aExtraHours,
aExtraMinutes, aExtraSeconds, aTime) {
@@ -4437,17 +4447,17 @@ ICAL.TimezoneService = (function() {
BYYEARDAY: parseNumericValue.bind(this, 'BYYEARDAY', -366, 366),
BYWEEKNO: parseNumericValue.bind(this, 'BYWEEKNO', -53, 53),
BYMONTH: parseNumericValue.bind(this, 'BYMONTH', 0, 12),
BYSETPOS: parseNumericValue.bind(this, 'BYSETPOS', -366, 366)
};
ICAL.Recur.fromString = function(string) {
var dict = Object.create(null);
- var dictParts = dict.parts = {};
+ var dictParts = dict.parts = Object.create(null);
// split is slower in FF but fast enough.
// v8 however this is faster then manual split?
var values = string.split(';');
var len = values.length;
for (var i = 0; i < len; i++) {
var parts = values[i].split('=');
@@ -4877,129 +4887,137 @@ ICAL.RecurIterator = (function() {
// only add unique items...
if (newRules.indexOf(rule) === -1) {
newRules.push(rule);
}
}
// unique and sort
- return newRules.sort();
+ return newRules.sort(function(a,b){return a - b});
},
/**
* NOTES:
* We are given a list of dates in the month (BYMONTHDAY) (23, etc..)
* Also we are given a list of days (BYDAY) (MO, 2SU, etc..) when
* both conditions match a given date (this.last.day) iteration stops.
*
* @param {Boolean} [isInit] when given true will not
* increment the current day (this.last).
*/
_byDayAndMonthDay: function(isInit) {
- var byMonthDay; // setup in initMonth
+ var byMonthDay; // setup in initMonth
var byDay = this.by_data.BYDAY;
var date;
var dateIdx = 0;
var dateLen; // setup in initMonth
- var dayIdx = 0;
var dayLen = byDay.length;
// we are not valid by default
var dataIsValid = 0;
var daysInMonth;
var self = this;
+ // we need a copy of this, because a DateTime gets normalized
+ // automatically if the day is out of range. At some points we
+ // set the last day to 0 to start counting.
+ var lastDay = this.last.day;
function initMonth() {
daysInMonth = ICAL.Time.daysInMonth(
self.last.month, self.last.year
);
byMonthDay = self.normalizeByMonthDayRules(
self.last.year,
self.last.month,
self.by_data.BYMONTHDAY
);
dateLen = byMonthDay.length;
+
+ // For the case of more than one occurrence in one month
+ // we have to be sure to start searching after the last
+ // found date or at the last BYMONTHDAY.
+ while (byMonthDay[dateIdx] <= lastDay && dateIdx < dateLen - 1) {
+ dateIdx++;
+ }
}
function nextMonth() {
- self.last.day = 1;
+ // since the day is incremented at the start
+ // of the loop below, we need to start at 0
+ lastDay = 0;
self.increment_month();
+ dateIdx = 0;
initMonth();
-
- dateIdx = 0;
- dayIdx = 0;
}
initMonth();
// should come after initMonth
if (isInit) {
- this.last.day -= 1;
+ lastDay -= 1;
}
while (!dataIsValid) {
- // find next date
- var next = byMonthDay[dateIdx++];
-
// increment the current date. This is really
// important otherwise we may fall into the infinite
// loop trap. The initial date takes care of the case
// where the current date is the date we are looking
// for.
- date = this.last.day + 1;
+ date = lastDay + 1;
if (date > daysInMonth) {
nextMonth();
continue;
}
- // after verify that the next date
- // is in the current month we can increment
- // it permanently.
- this.last.day = date;
+ // find next date
+ var next = byMonthDay[dateIdx++];
// this logic is dependant on the BYMONTHDAYS
// being in order (which is done by #normalizeByMonthDayRules)
- if (next >= this.last.day) {
+ if (next >= date) {
// if the next month day is in the future jump to it.
- this.last.day = next;
+ lastDay = next;
} else {
// in this case the 'next' monthday has past
// we must move to the month.
nextMonth();
continue;
}
// Now we can loop through the day rules to see
// if one matches the current month date.
- for (dayIdx = 0; dayIdx < dayLen; dayIdx++) {
+ for (var dayIdx = 0; dayIdx < dayLen; dayIdx++) {
var parts = this.ruleDayOfWeek(byDay[dayIdx]);
var pos = parts[0];
var dow = parts[1];
+ this.last.day = lastDay;
if (this.last.isNthWeekDay(dow, pos)) {
// when we find the valid one we can mark
// the conditions as met and break the loop.
// (Because we have this condition above
// it will also break the parent loop).
dataIsValid = 1;
break;
}
}
// Its completely possible that the combination
// cannot be matched in the current month.
// When we reach the end of possible combinations
// in the current month we iterate to the next one.
- if (!dataIsValid && dateIdx === (dateLen - 1)) {
+ // since dateIdx is incremented right after getting
+ // "next", we don't need dateLen -1 here.
+ if (!dataIsValid && dateIdx === dateLen) {
nextMonth();
continue;
}
}
return dataIsValid;
},
@@ -5068,19 +5086,20 @@ ICAL.RecurIterator = (function() {
if (day < 0) {
day = daysInMonth + day + 1;
}
if (day > daysInMonth) {
this.last.day = 1;
data_valid = this.is_day_in_byday(this.last);
+ } else {
+ this.last.day = day;
}
- this.last.day = day;
} else {
this.last.day = this.by_data.BYMONTHDAY[0];
this.increment_month();
var daysInMonth = ICAL.Time.daysInMonth(this.last.month, this.last.year);
this.last.day = Math.min(this.last.day, daysInMonth);
}
return data_valid;
@@ -5096,17 +5115,17 @@ ICAL.RecurIterator = (function() {
if (!this.has_by_data("BYDAY")) {
return 1;
}
for (;;) {
var tt = new ICAL.Time();
this.by_indices.BYDAY++;
- if (this.by_indices.BYDAY == this.by_data.BYDAY.length) {
+ if (this.by_indices.BYDAY == Object.keys(this.by_data.BYDAY).length) {
this.by_indices.BYDAY = 0;
end_of_data = 1;
}
var coded_day = this.by_data.BYDAY[this.by_indices.BYDAY];
var parts = this.ruleDayOfWeek(coded_day);
var dow = parts[1];
@@ -5218,27 +5237,27 @@ ICAL.RecurIterator = (function() {
if (this.last.day > daysInMonth) {
this.last.day -= daysInMonth;
this.increment_month();
}
}
},
increment_month: function increment_month() {
+ this.last.day = 1;
if (this.has_by_data("BYMONTH")) {
this.by_indices.BYMONTH++;
if (this.by_indices.BYMONTH == this.by_data.BYMONTH.length) {
this.by_indices.BYMONTH = 0;
this.increment_year(1);
}
this.last.month = this.by_data.BYMONTH[this.by_indices.BYMONTH];
} else {
- var inc;
if (this.rule.freq == "MONTHLY") {
this.last.month += this.rule.interval;
} else {
this.last.month++;
}
this.last.month--;
var years = ICAL.helpers.trunc(this.last.month / 12);
@@ -6277,26 +6296,32 @@ ICAL.Event = (function() {
*
* NOTE: this method is intend to be used in conjunction
* with the #iterator method.
*
* @param {ICAL.Time} occurrence time occurrence.
*/
getOccurrenceDetails: function(occurrence) {
var id = occurrence.toString();
+ var utcId = occurrence.convertToZone(ICAL.Timezone.utcTimezone).toString();
var result = {
//XXX: Clone?
recurrenceId: occurrence
};
if (id in this.exceptions) {
var item = result.item = this.exceptions[id];
result.startDate = item.startDate;
result.endDate = item.endDate;
result.item = item;
+ } else if (utcId in this.exceptions) {
+ var item = this.exceptions[utcId];
+ result.startDate = item.startDate;
+ result.endDate = item.endDate;
+ result.item = item;
} else {
// range exceptions (RANGE=THISANDFUTURE) have a
// lower priority then direct exceptions but
// must be accounted for first. Their item is
// always the first exception with the range prop.
var rangeExceptionId = this.findRangeException(
occurrence
);