Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
6671 lines
174 KiB
JavaScript
Executable file
6671 lines
174 KiB
JavaScript
Executable file
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
* Portions Copyright (C) Philipp Kewisch, 2011-2012 */
|
|
|
|
if (typeof ICAL === 'undefined') {
|
|
if (typeof exports === 'object') {
|
|
// CommonJS
|
|
ICAL = exports;
|
|
} else if (typeof window !== 'undefined') {
|
|
// Browser globals
|
|
this.ICAL = {};
|
|
} else {
|
|
// ...?
|
|
ICAL = {};
|
|
}
|
|
}
|
|
|
|
ICAL.foldLength = 75;
|
|
ICAL.newLineChar = '\r\n';
|
|
|
|
/**
|
|
* Helper functions used in various places within ical.js
|
|
*/
|
|
ICAL.helpers = {
|
|
initState: function initState(aLine, aLineNr) {
|
|
return {
|
|
buffer: aLine,
|
|
line: aLine,
|
|
lineNr: aLineNr,
|
|
character: 0,
|
|
currentData: null,
|
|
parentData: []
|
|
};
|
|
},
|
|
|
|
initComponentData: function initComponentData(aName) {
|
|
return {
|
|
name: aName,
|
|
type: "COMPONENT",
|
|
value: []
|
|
};
|
|
},
|
|
|
|
/**
|
|
* Checks if the given number is NaN
|
|
*/
|
|
isStrictlyNaN: function(number) {
|
|
return typeof(number) === 'number' && isNaN(number);
|
|
},
|
|
|
|
/**
|
|
* Parses a string value that is expected to be an
|
|
* integer, when the valid is not an integer throws
|
|
* a decoration error.
|
|
*
|
|
* @param {String} string raw input.
|
|
* @return {Number} integer.
|
|
*/
|
|
strictParseInt: function(string) {
|
|
var result = parseInt(string, 10);
|
|
|
|
if (ICAL.helpers.isStrictlyNaN(result)) {
|
|
throw new Error(
|
|
'Could not extract integer from "' + string + '"'
|
|
);
|
|
}
|
|
|
|
return result;
|
|
},
|
|
|
|
/**
|
|
* Creates or returns a class instance
|
|
* of a given type with the initialization
|
|
* data if the data is not already an instance
|
|
* of the given type.
|
|
*
|
|
*
|
|
* Example:
|
|
*
|
|
* var time = new ICAL.Time(...);
|
|
* var result = ICAL.helpers.formatClassType(time, ICAL.Time);
|
|
*
|
|
* (result instanceof ICAL.Time)
|
|
* // => true
|
|
*
|
|
* result = ICAL.helpers.formatClassType({}, ICAL.Time);
|
|
* (result isntanceof ICAL.Time)
|
|
* // => true
|
|
*
|
|
*
|
|
* @param {Object} data object initialization data.
|
|
* @param {Object} type object type (like ICAL.Time).
|
|
*/
|
|
formatClassType: function formatClassType(data, type) {
|
|
if (typeof(data) === 'undefined')
|
|
return undefined;
|
|
|
|
if (data instanceof type) {
|
|
return data;
|
|
}
|
|
return new type(data);
|
|
},
|
|
|
|
/**
|
|
* Identical to index of but will only match values
|
|
* when they are not preceded by a backslash char \\\
|
|
*
|
|
* @param {String} buffer string value.
|
|
* @param {String} search value.
|
|
* @param {Numeric} pos start position.
|
|
*/
|
|
unescapedIndexOf: function(buffer, search, pos) {
|
|
while ((pos = buffer.indexOf(search, pos)) !== -1) {
|
|
if (pos > 0 && buffer[pos - 1] === '\\') {
|
|
pos += 1;
|
|
} else {
|
|
return pos;
|
|
}
|
|
}
|
|
return -1;
|
|
},
|
|
|
|
binsearchInsert: function(list, seekVal, cmpfunc) {
|
|
if (!list.length)
|
|
return 0;
|
|
|
|
var low = 0, high = list.length - 1,
|
|
mid, cmpval;
|
|
|
|
while (low <= high) {
|
|
mid = low + Math.floor((high - low) / 2);
|
|
cmpval = cmpfunc(seekVal, list[mid]);
|
|
|
|
if (cmpval < 0)
|
|
high = mid - 1;
|
|
else if (cmpval > 0)
|
|
low = mid + 1;
|
|
else
|
|
break;
|
|
}
|
|
|
|
if (cmpval < 0)
|
|
return mid; // insertion is displacing, so use mid outright.
|
|
else if (cmpval > 0)
|
|
return mid + 1;
|
|
else
|
|
return mid;
|
|
},
|
|
|
|
dumpn: function() {
|
|
if (!ICAL.debug) {
|
|
return null;
|
|
}
|
|
|
|
if (typeof (console) !== 'undefined' && 'log' in console) {
|
|
ICAL.helpers.dumpn = function consoleDumpn(input) {
|
|
return console.log(input);
|
|
}
|
|
} else {
|
|
ICAL.helpers.dumpn = function geckoDumpn(input) {
|
|
dump(input + '\n');
|
|
}
|
|
}
|
|
|
|
return ICAL.helpers.dumpn(arguments[0]);
|
|
},
|
|
|
|
mixin: function(obj, data) {
|
|
if (data) {
|
|
for (var k in data) {
|
|
obj[k] = data[k];
|
|
}
|
|
}
|
|
return obj;
|
|
},
|
|
|
|
isArray: function(o) {
|
|
return o && (o instanceof Array || typeof o == "array");
|
|
},
|
|
|
|
clone: function(aSrc, aDeep) {
|
|
if (!aSrc || typeof aSrc != "object") {
|
|
return aSrc;
|
|
} else if (aSrc instanceof Date) {
|
|
return new Date(aSrc.getTime());
|
|
} else if ("clone" in aSrc) {
|
|
return aSrc.clone();
|
|
} else if (ICAL.helpers.isArray(aSrc)) {
|
|
var result = [];
|
|
for (var i = 0; i < aSrc.length; i++) {
|
|
result.push(aDeep ? ICAL.helpers.clone(aSrc[i], true) : aSrc[i]);
|
|
}
|
|
return result;
|
|
} else {
|
|
var result = {};
|
|
for (var name in aSrc) {
|
|
// uses prototype method to allow use of Object.create(null);
|
|
if (Object.prototype.hasOwnProperty.call(aSrc, name)) {
|
|
if (aDeep) {
|
|
result[name] = ICAL.helpers.clone(aSrc[name], true);
|
|
} else {
|
|
result[name] = aSrc[name];
|
|
}
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
},
|
|
|
|
unfoldline: function unfoldline(aState) {
|
|
// Section 3.1
|
|
// if the line ends with a CRLF
|
|
// and the next line starts with a LINEAR WHITESPACE (space, htab, ...)
|
|
|
|
// then remove the CRLF and the whitespace to unsplit the line
|
|
var moreLines = true;
|
|
var line = "";
|
|
|
|
while (moreLines) {
|
|
moreLines = false;
|
|
var pos = aState.buffer.search(/\r?\n/);
|
|
if (pos > -1) {
|
|
var len = (aState.buffer[pos] == "\r" ? 2 : 1);
|
|
var nextChar = aState.buffer.substr(pos + len, 1);
|
|
if (nextChar.match(/^[ \t]$/)) {
|
|
moreLines = true;
|
|
line += aState.buffer.substr(0, pos);
|
|
aState.buffer = aState.buffer.substr(pos + len + 1);
|
|
} else {
|
|
// We're at the end of the line, copy the found chunk
|
|
line += aState.buffer.substr(0, pos);
|
|
aState.buffer = aState.buffer.substr(pos + len);
|
|
}
|
|
} else {
|
|
line += aState.buffer;
|
|
aState.buffer = "";
|
|
}
|
|
}
|
|
return line;
|
|
},
|
|
|
|
foldline: function foldline(aLine) {
|
|
var result = "";
|
|
var line = aLine || "";
|
|
|
|
while (line.length) {
|
|
result += ICAL.newLineChar + " " + line.substr(0, ICAL.foldLength);
|
|
line = line.substr(ICAL.foldLength);
|
|
}
|
|
return result.substr(ICAL.newLineChar.length + 1);
|
|
},
|
|
|
|
ensureKeyExists: function(obj, key, defvalue) {
|
|
if (!(key in obj)) {
|
|
obj[key] = defvalue;
|
|
}
|
|
},
|
|
|
|
hasKey: function(obj, key) {
|
|
return (obj && key in obj && obj[key]);
|
|
},
|
|
|
|
pad2: function pad(data) {
|
|
if (typeof(data) !== 'string') {
|
|
// handle fractions.
|
|
if (typeof(data) === 'number') {
|
|
data = parseInt(data);
|
|
}
|
|
data = String(data);
|
|
}
|
|
|
|
var len = data.length;
|
|
|
|
switch (len) {
|
|
case 0:
|
|
return '00';
|
|
case 1:
|
|
return '0' + data;
|
|
default:
|
|
return data;
|
|
}
|
|
},
|
|
|
|
trunc: function trunc(number) {
|
|
return (number < 0 ? Math.ceil(number) : Math.floor(number));
|
|
}
|
|
};
|
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
* Portions Copyright (C) Philipp Kewisch, 2011-2012 */
|
|
|
|
(typeof(ICAL) === 'undefined')? ICAL = {} : '';
|
|
|
|
ICAL.design = (function() {
|
|
'use strict';
|
|
|
|
var ICAL_NEWLINE = /\\\\|\\;|\\,|\\[Nn]/g;
|
|
|
|
function DecorationError() {
|
|
Error.apply(this, arguments);
|
|
}
|
|
|
|
DecorationError.prototype = {
|
|
__proto__: Error.prototype
|
|
};
|
|
|
|
function replaceNewlineReplace(string) {
|
|
switch (string) {
|
|
case "\\\\":
|
|
return "\\";
|
|
case "\\;":
|
|
return ";";
|
|
case "\\,":
|
|
return ",";
|
|
case "\\n":
|
|
case "\\N":
|
|
return "\n";
|
|
default:
|
|
return string;
|
|
}
|
|
}
|
|
|
|
function replaceNewline(value) {
|
|
// avoid regex when possible.
|
|
if (value.indexOf('\\') === -1) {
|
|
return value;
|
|
}
|
|
|
|
return value.replace(ICAL_NEWLINE, replaceNewlineReplace);
|
|
}
|
|
|
|
/**
|
|
* Changes the format of the UNTIl part in the RECUR
|
|
* value type. When no UNTIL part is found the original
|
|
* is returned untouched.
|
|
*
|
|
* @param {String} type toICAL or fromICAL.
|
|
* @param {String} aValue the value to check.
|
|
* @return {String} upgraded/original value.
|
|
*/
|
|
function recurReplaceUntil(aType, aValue) {
|
|
var idx = aValue.indexOf('UNTIL=');
|
|
if (idx === -1) {
|
|
return aValue;
|
|
}
|
|
|
|
idx += 6;
|
|
|
|
// everything before the value
|
|
var begin = aValue.substr(0, idx);
|
|
|
|
// everything after the value
|
|
var end;
|
|
|
|
// current until value
|
|
var until;
|
|
|
|
// end of value could be -1 meaning this is the last param.
|
|
var endValueIdx = aValue.indexOf(';', idx);
|
|
|
|
if (endValueIdx === -1) {
|
|
end = '';
|
|
until = aValue.substr(idx);
|
|
} else {
|
|
end = aValue.substr(endValueIdx);
|
|
until = aValue.substr(idx, endValueIdx - idx);
|
|
}
|
|
|
|
if (until.length > 10) {
|
|
until = design.value['date-time'][aType](until);
|
|
} else {
|
|
until = design.value.date[aType](until);
|
|
}
|
|
|
|
return begin + until + end;
|
|
}
|
|
/**
|
|
* Design data used by the parser to decide if data is semantically correct
|
|
*/
|
|
var design = {
|
|
DecorationError: DecorationError,
|
|
|
|
defaultType: 'text',
|
|
|
|
param: {
|
|
// Although the syntax is DQUOTE uri DQUOTE, I don't think we should
|
|
// enfoce anything aside from it being a valid content line.
|
|
// "ALTREP": { ... },
|
|
|
|
// CN just wants a param-value
|
|
// "CN": { ... }
|
|
|
|
"cutype": {
|
|
values: ["INDIVIDUAL", "GROUP", "RESOURCE", "ROOM", "UNKNOWN"],
|
|
allowXName: true,
|
|
allowIanaToken: true
|
|
},
|
|
|
|
"delegated-from": {
|
|
valueType: "cal-address",
|
|
multiValue: ","
|
|
},
|
|
"delegated-to": {
|
|
valueType: "cal-address",
|
|
multiValue: ","
|
|
},
|
|
// "DIR": { ... }, // See ALTREP
|
|
"encoding": {
|
|
values: ["8BIT", "BASE64"]
|
|
},
|
|
// "FMTTYPE": { ... }, // See ALTREP
|
|
"fbtype": {
|
|
values: ["FREE", "BUSY", "BUSY-UNAVAILABLE", "BUSY-TENTATIVE"],
|
|
allowXName: true,
|
|
allowIanaToken: true
|
|
},
|
|
// "LANGUAGE": { ... }, // See ALTREP
|
|
"member": {
|
|
valueType: "cal-address",
|
|
multiValue: ","
|
|
},
|
|
"partstat": {
|
|
// TODO These values are actually different per-component
|
|
values: ["NEEDS-ACTION", "ACCEPTED", "DECLINED", "TENTATIVE",
|
|
"DELEGATED", "COMPLETED", "IN-PROCESS"],
|
|
allowXName: true,
|
|
allowIanaToken: true
|
|
},
|
|
"range": {
|
|
values: ["THISLANDFUTURE"]
|
|
},
|
|
"related": {
|
|
values: ["START", "END"]
|
|
},
|
|
"reltype": {
|
|
values: ["PARENT", "CHILD", "SIBLING"],
|
|
allowXName: true,
|
|
allowIanaToken: true
|
|
},
|
|
"role": {
|
|
values: ["REQ-PARTICIPANT", "CHAIR",
|
|
"OPT-PARTICIPANT", "NON-PARTICIPANT"],
|
|
allowXName: true,
|
|
allowIanaToken: true
|
|
},
|
|
"rsvp": {
|
|
valueType: "boolean"
|
|
},
|
|
"sent-by": {
|
|
valueType: "cal-address"
|
|
},
|
|
"tzid": {
|
|
matches: /^\//
|
|
},
|
|
"value": {
|
|
// since the value here is a 'type' lowercase is used.
|
|
values: ["binary", "boolean", "cal-address", "date", "date-time",
|
|
"duration", "float", "integer", "period", "recur", "text",
|
|
"time", "uri", "utc-offset"],
|
|
allowXName: true,
|
|
allowIanaToken: true
|
|
}
|
|
},
|
|
|
|
// When adding a value here, be sure to add it to the parameter types!
|
|
value: {
|
|
|
|
"binary": {
|
|
decorate: function(aString) {
|
|
return ICAL.Binary.fromString(aString);
|
|
},
|
|
|
|
undecorate: function(aBinary) {
|
|
return aBinary.toString();
|
|
}
|
|
},
|
|
"boolean": {
|
|
values: ["TRUE", "FALSE"],
|
|
|
|
fromICAL: function(aValue) {
|
|
switch(aValue) {
|
|
case 'TRUE':
|
|
return true;
|
|
case 'FALSE':
|
|
return false;
|
|
default:
|
|
//TODO: parser warning
|
|
return false;
|
|
}
|
|
},
|
|
|
|
toICAL: function(aValue) {
|
|
if (aValue) {
|
|
return 'TRUE';
|
|
}
|
|
return 'FALSE';
|
|
}
|
|
|
|
},
|
|
"cal-address": {
|
|
// needs to be an uri
|
|
},
|
|
"date": {
|
|
decorate: function(aValue, aProp) {
|
|
return ICAL.Time.fromDateString(aValue, aProp);
|
|
},
|
|
|
|
/**
|
|
* undecorates a time object.
|
|
*/
|
|
undecorate: function(aValue) {
|
|
return aValue.toString();
|
|
},
|
|
|
|
fromICAL: function(aValue) {
|
|
// from: 20120901
|
|
// to: 2012-09-01
|
|
var result = aValue.substr(0, 4) + '-' +
|
|
aValue.substr(4, 2) + '-' +
|
|
aValue.substr(6, 2);
|
|
|
|
if (aValue[8] === 'Z') {
|
|
result += 'Z';
|
|
}
|
|
|
|
return result;
|
|
},
|
|
|
|
toICAL: function(aValue) {
|
|
// from: 2012-09-01
|
|
// to: 20120901
|
|
|
|
if (aValue.length > 11) {
|
|
//TODO: serialize warning?
|
|
return aValue;
|
|
}
|
|
|
|
var result = aValue.substr(0, 4) +
|
|
aValue.substr(5, 2) +
|
|
aValue.substr(8, 2);
|
|
|
|
if (aValue[10] === 'Z') {
|
|
result += 'Z';
|
|
}
|
|
|
|
return result;
|
|
}
|
|
},
|
|
"date-time": {
|
|
fromICAL: function(aValue) {
|
|
// from: 20120901T130000
|
|
// to: 2012-09-01T13:00:00
|
|
var result = aValue.substr(0, 4) + '-' +
|
|
aValue.substr(4, 2) + '-' +
|
|
aValue.substr(6, 2) + 'T' +
|
|
aValue.substr(9, 2) + ':' +
|
|
aValue.substr(11, 2) + ':' +
|
|
aValue.substr(13, 2);
|
|
|
|
if (aValue[15] === 'Z') {
|
|
result += 'Z'
|
|
}
|
|
|
|
return result;
|
|
},
|
|
|
|
toICAL: function(aValue) {
|
|
// from: 2012-09-01T13:00:00
|
|
// to: 20120901T130000
|
|
|
|
if (aValue.length < 19) {
|
|
// TODO: error
|
|
return aValue;
|
|
}
|
|
|
|
var result = aValue.substr(0, 4) +
|
|
aValue.substr(5, 2) +
|
|
// grab the (DDTHH) segment
|
|
aValue.substr(8, 5) +
|
|
// MM
|
|
aValue.substr(14, 2) +
|
|
// SS
|
|
aValue.substr(17, 2);
|
|
|
|
if (aValue[19] === 'Z') {
|
|
result += 'Z';
|
|
}
|
|
|
|
return result;
|
|
},
|
|
|
|
decorate: function(aValue, aProp) {
|
|
return ICAL.Time.fromDateTimeString(aValue, aProp);
|
|
},
|
|
|
|
undecorate: function(aValue) {
|
|
return aValue.toString();
|
|
}
|
|
},
|
|
duration: {
|
|
decorate: function(aValue) {
|
|
return ICAL.Duration.fromString(aValue);
|
|
},
|
|
undecorate: function(aValue) {
|
|
return aValue.toString();
|
|
}
|
|
},
|
|
float: {
|
|
matches: /^[+-]?\d+\.\d+$/,
|
|
decorate: function(aValue) {
|
|
return ICAL.Value.fromString(aValue, "float");
|
|
},
|
|
|
|
fromICAL: function(aValue) {
|
|
var parsed = parseFloat(aValue);
|
|
if (ICAL.helpers.isStrictlyNaN(parsed)) {
|
|
// TODO: parser warning
|
|
return 0.0;
|
|
}
|
|
return parsed;
|
|
},
|
|
|
|
toICAL: function(aValue) {
|
|
return String(aValue);
|
|
}
|
|
},
|
|
integer: {
|
|
fromICAL: function(aValue) {
|
|
var parsed = parseInt(aValue);
|
|
if (ICAL.helpers.isStrictlyNaN(parsed)) {
|
|
return 0;
|
|
}
|
|
return parsed;
|
|
},
|
|
|
|
toICAL: function(aValue) {
|
|
return String(aValue);
|
|
}
|
|
},
|
|
period: {
|
|
|
|
fromICAL: function(string) {
|
|
var parts = string.split('/');
|
|
var result = design.value['date-time'].fromICAL(parts[0]) + '/';
|
|
|
|
if (ICAL.Duration.isValueString(parts[1])) {
|
|
result += parts[1];
|
|
} else {
|
|
result += design.value['date-time'].fromICAL(parts[1]);
|
|
}
|
|
|
|
return result;
|
|
},
|
|
|
|
toICAL: function(string) {
|
|
var parts = string.split('/');
|
|
var result = design.value['date-time'].toICAL(parts[0]) + '/';
|
|
|
|
if (ICAL.Duration.isValueString(parts[1])) {
|
|
result += parts[1];
|
|
} else {
|
|
result += design.value['date-time'].toICAL(parts[1]);
|
|
}
|
|
|
|
return result;
|
|
},
|
|
|
|
decorate: function(aValue, aProp) {
|
|
return ICAL.Period.fromString(aValue, aProp);
|
|
},
|
|
|
|
undecorate: function(aValue) {
|
|
return aValue.toString();
|
|
}
|
|
},
|
|
recur: {
|
|
fromICAL: recurReplaceUntil.bind(this, 'fromICAL'),
|
|
toICAL: recurReplaceUntil.bind(this, 'toICAL'),
|
|
|
|
decorate: function decorate(aValue) {
|
|
return ICAL.Recur.fromString(aValue);
|
|
},
|
|
|
|
undecorate: function(aRecur) {
|
|
return aRecur.toString();
|
|
}
|
|
},
|
|
|
|
text: {
|
|
matches: /.*/,
|
|
|
|
fromICAL: function(aValue, aName) {
|
|
return replaceNewline(aValue);
|
|
},
|
|
|
|
toICAL: function escape(aValue, aName) {
|
|
return aValue.replace(/\\|;|,|\n/g, function(str) {
|
|
switch (str) {
|
|
case "\\":
|
|
return "\\\\";
|
|
case ";":
|
|
return "\\;";
|
|
case ",":
|
|
return "\\,";
|
|
case "\n":
|
|
return "\\n";
|
|
default:
|
|
return str;
|
|
}
|
|
});
|
|
}
|
|
},
|
|
|
|
time: {
|
|
fromICAL: function(aValue) {
|
|
// from: MMHHSS(Z)?
|
|
// to: HH:MM:SS(Z)?
|
|
if (aValue.length < 6) {
|
|
// TODO: parser exception?
|
|
return aValue;
|
|
}
|
|
|
|
// HH::MM::SSZ?
|
|
var result = aValue.substr(0, 2) + ':' +
|
|
aValue.substr(2, 2) + ':' +
|
|
aValue.substr(4, 2);
|
|
|
|
if (aValue[6] === 'Z') {
|
|
result += 'Z';
|
|
}
|
|
|
|
return result;
|
|
},
|
|
|
|
toICAL: function(aValue) {
|
|
// from: HH:MM:SS(Z)?
|
|
// to: MMHHSS(Z)?
|
|
if (aValue.length < 8) {
|
|
//TODO: error
|
|
return aValue;
|
|
}
|
|
|
|
var result = aValue.substr(0, 2) +
|
|
aValue.substr(3, 2) +
|
|
aValue.substr(6, 2);
|
|
|
|
if (aValue[8] === 'Z') {
|
|
result += 'Z';
|
|
}
|
|
|
|
return result;
|
|
}
|
|
},
|
|
|
|
uri: {
|
|
// TODO
|
|
/* ... */
|
|
},
|
|
|
|
"utc-offset": {
|
|
toICAL: function(aValue) {
|
|
if (aValue.length < 7) {
|
|
// no seconds
|
|
// -0500
|
|
return aValue.substr(0, 3) +
|
|
aValue.substr(4, 2);
|
|
} else {
|
|
// seconds
|
|
// -050000
|
|
return aValue.substr(0, 3) +
|
|
aValue.substr(4, 2) +
|
|
aValue.substr(7, 2);
|
|
}
|
|
},
|
|
|
|
fromICAL: function(aValue) {
|
|
if (aValue.length < 6) {
|
|
// no seconds
|
|
// -05:00
|
|
return aValue.substr(0, 3) + ':' +
|
|
aValue.substr(3, 2);
|
|
} else {
|
|
// seconds
|
|
// -05:00:00
|
|
return aValue.substr(0, 3) + ':' +
|
|
aValue.substr(3, 2) + ':' +
|
|
aValue.substr(5, 2);
|
|
}
|
|
},
|
|
|
|
decorate: function(aValue) {
|
|
return ICAL.UtcOffset.fromString(aValue);
|
|
},
|
|
|
|
undecorate: function(aValue) {
|
|
return aValue.toString();
|
|
}
|
|
}
|
|
},
|
|
|
|
property: {
|
|
decorate: function decorate(aData, aParent) {
|
|
return new ICAL.Property(aData, aParent);
|
|
},
|
|
"attach": {
|
|
defaultType: "uri"
|
|
},
|
|
"attendee": {
|
|
defaultType: "cal-address"
|
|
},
|
|
"categories": {
|
|
defaultType: "text",
|
|
multiValue: ","
|
|
},
|
|
"completed": {
|
|
defaultType: "date-time"
|
|
},
|
|
"created": {
|
|
defaultType: "date-time"
|
|
},
|
|
"dtend": {
|
|
defaultType: "date-time",
|
|
allowedTypes: ["date-time", "date"]
|
|
},
|
|
"dtstamp": {
|
|
defaultType: "date-time"
|
|
},
|
|
"dtstart": {
|
|
defaultType: "date-time",
|
|
allowedTypes: ["date-time", "date"]
|
|
},
|
|
"due": {
|
|
defaultType: "date-time",
|
|
allowedTypes: ["date-time", "date"]
|
|
},
|
|
"duration": {
|
|
defaultType: "duration"
|
|
},
|
|
"exdate": {
|
|
defaultType: "date-time",
|
|
allowedTypes: ["date-time", "date"],
|
|
multiValue: ','
|
|
},
|
|
"exrule": {
|
|
defaultType: "recur"
|
|
},
|
|
"freebusy": {
|
|
defaultType: "period",
|
|
multiValue: ","
|
|
},
|
|
"geo": {
|
|
defaultType: "float",
|
|
multiValue: ";"
|
|
},
|
|
/* TODO exactly 2 values */"last-modified": {
|
|
defaultType: "date-time"
|
|
},
|
|
"organizer": {
|
|
defaultType: "cal-address"
|
|
},
|
|
"percent-complete": {
|
|
defaultType: "integer"
|
|
},
|
|
"repeat": {
|
|
defaultType: "integer"
|
|
},
|
|
"rdate": {
|
|
defaultType: "date-time",
|
|
allowedTypes: ["date-time", "date", "period"],
|
|
multiValue: ',',
|
|
detectType: function(string) {
|
|
if (string.indexOf('/') !== -1) {
|
|
return 'period';
|
|
}
|
|
return (string.indexOf('T') === -1) ? 'date' : 'date-time';
|
|
}
|
|
},
|
|
"recurrence-id": {
|
|
defaultType: "date-time",
|
|
allowedTypes: ["date-time", "date"]
|
|
},
|
|
"resources": {
|
|
defaultType: "text",
|
|
multiValue: ","
|
|
},
|
|
"request-status": {
|
|
defaultType: "text",
|
|
multiValue: ";"
|
|
},
|
|
"priority": {
|
|
defaultType: "integer"
|
|
},
|
|
"rrule": {
|
|
defaultType: "recur"
|
|
},
|
|
"sequence": {
|
|
defaultType: "integer"
|
|
},
|
|
"trigger": {
|
|
defaultType: "duration",
|
|
allowedTypes: ["duration", "date-time"]
|
|
},
|
|
"tzoffsetfrom": {
|
|
defaultType: "utc-offset"
|
|
},
|
|
"tzoffsetto": {
|
|
defaultType: "utc-offset"
|
|
},
|
|
"tzurl": {
|
|
defaultType: "uri"
|
|
},
|
|
"url": {
|
|
defaultType: "uri"
|
|
}
|
|
},
|
|
|
|
component: {
|
|
decorate: function decorate(aData, aParent) {
|
|
return new ICAL.Component(aData, aParent);
|
|
},
|
|
"vevent": {}
|
|
}
|
|
|
|
};
|
|
|
|
return design;
|
|
}());
|
|
ICAL.stringify = (function() {
|
|
'use strict';
|
|
|
|
var LINE_ENDING = '\r\n';
|
|
var DEFAULT_TYPE = 'text';
|
|
|
|
var design = ICAL.design;
|
|
var helpers = ICAL.helpers;
|
|
|
|
/**
|
|
* Convert a full jCal Array into a ical document.
|
|
*
|
|
* @param {Array} jCal document.
|
|
* @return {String} ical document.
|
|
*/
|
|
function stringify(jCal) {
|
|
if (!jCal[0] || jCal[0] !== 'icalendar') {
|
|
throw new Error('must provide full jCal document');
|
|
}
|
|
|
|
// 1 because we skip the initial element.
|
|
var i = 1;
|
|
var len = jCal.length;
|
|
var result = '';
|
|
|
|
for (; i < len; i++) {
|
|
result += stringify.component(jCal[i]) + LINE_ENDING;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Converts an jCal component array into a ICAL string.
|
|
* Recursive will resolve sub-components.
|
|
*
|
|
* Exact component/property order is not saved all
|
|
* properties will come before subcomponents.
|
|
*
|
|
* @param {Array} component jCal fragment of a component.
|
|
*/
|
|
stringify.component = function(component) {
|
|
var name = component[0].toUpperCase();
|
|
var result = 'BEGIN:' + name + LINE_ENDING;
|
|
|
|
var props = component[1];
|
|
var propIdx = 0;
|
|
var propLen = props.length;
|
|
|
|
for (; propIdx < propLen; propIdx++) {
|
|
result += stringify.property(props[propIdx]) + LINE_ENDING;
|
|
}
|
|
|
|
var comps = component[2];
|
|
var compIdx = 0;
|
|
var compLen = comps.length;
|
|
|
|
for (; compIdx < compLen; compIdx++) {
|
|
result += stringify.component(comps[compIdx]) + LINE_ENDING;
|
|
}
|
|
|
|
result += 'END:' + name;
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Converts a single property to a ICAL string.
|
|
*
|
|
* @param {Array} property jCal property.
|
|
*/
|
|
stringify.property = function(property) {
|
|
var name = property[0].toUpperCase();
|
|
var jsName = property[0];
|
|
var params = property[1];
|
|
|
|
var line = name;
|
|
|
|
var paramName;
|
|
for (paramName in params) {
|
|
if (params.hasOwnProperty(paramName)) {
|
|
line += ';' + paramName.toUpperCase();
|
|
line += '=' + stringify.propertyValue(params[paramName]);
|
|
}
|
|
}
|
|
|
|
// there is no value so return.
|
|
if (property.length === 3) {
|
|
// if no params where inserted and no value
|
|
// we given we must add a blank value.
|
|
if (!paramName) {
|
|
line += ':';
|
|
}
|
|
return line;
|
|
}
|
|
|
|
var valueType = property[2];
|
|
|
|
var propDetails;
|
|
var multiValue = false;
|
|
var isDefault = false;
|
|
|
|
if (jsName in design.property) {
|
|
propDetails = design.property[jsName];
|
|
|
|
if ('multiValue' in propDetails) {
|
|
multiValue = propDetails.multiValue;
|
|
}
|
|
|
|
if ('defaultType' in propDetails) {
|
|
if (valueType === propDetails.defaultType) {
|
|
isDefault = true;
|
|
}
|
|
} else {
|
|
if (valueType === DEFAULT_TYPE) {
|
|
isDefault = true;
|
|
}
|
|
}
|
|
} else {
|
|
if (valueType === DEFAULT_TYPE) {
|
|
isDefault = true;
|
|
}
|
|
}
|
|
|
|
// push the VALUE property if type is not the default
|
|
// for the current property.
|
|
if (!isDefault) {
|
|
// value will never contain ;/:/, so we don't escape it here.
|
|
line += ';VALUE=' + valueType.toUpperCase();
|
|
}
|
|
|
|
line += ':';
|
|
|
|
if (multiValue) {
|
|
line += stringify.multiValue(
|
|
property.slice(3), multiValue, valueType
|
|
);
|
|
} else {
|
|
line += stringify.value(property[3], valueType);
|
|
}
|
|
|
|
return ICAL.helpers.foldline(line);
|
|
}
|
|
|
|
/**
|
|
* Handles escaping of property values that may contain:
|
|
*
|
|
* COLON (:), SEMICOLON (;), or COMMA (,)
|
|
*
|
|
* If any of the above are present the result is wrapped
|
|
* in double quotes.
|
|
*
|
|
* @param {String} value raw value.
|
|
* @return {String} given or escaped value when needed.
|
|
*/
|
|
stringify.propertyValue = function(value) {
|
|
|
|
if ((helpers.unescapedIndexOf(value, ',') === -1) &&
|
|
(helpers.unescapedIndexOf(value, ':') === -1) &&
|
|
(helpers.unescapedIndexOf(value, ';') === -1)) {
|
|
|
|
return value;
|
|
}
|
|
|
|
return '"' + value + '"';
|
|
}
|
|
|
|
/**
|
|
* Converts an array of ical values into a single
|
|
* string based on a type and a delimiter value (like ",").
|
|
*
|
|
* @param {Array} values list of values to convert.
|
|
* @param {String} delim used to join the values usually (",", ";", ":").
|
|
* @param {String} type lowecase ical value type
|
|
* (like boolean, date-time, etc..).
|
|
*
|
|
* @return {String} ical string for value.
|
|
*/
|
|
stringify.multiValue = function(values, delim, type) {
|
|
var result = '';
|
|
var len = values.length;
|
|
var i = 0;
|
|
|
|
for (; i < len; i++) {
|
|
result += stringify.value(values[i], type);
|
|
if (i !== (len - 1)) {
|
|
result += delim;
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Processes a single ical value runs the associated "toICAL"
|
|
* method from the design value type if available to convert
|
|
* the value.
|
|
*
|
|
* @param {String|Numeric} value some formatted value.
|
|
* @param {String} type lowecase ical value type
|
|
* (like boolean, date-time, etc..).
|
|
* @return {String} ical value for single value.
|
|
*/
|
|
stringify.value = function(value, type) {
|
|
if (type in design.value && 'toICAL' in design.value[type]) {
|
|
return design.value[type].toICAL(value);
|
|
}
|
|
return value;
|
|
}
|
|
|
|
return stringify;
|
|
|
|
}());
|
|
|
|
ICAL.parse = (function() {
|
|
'use strict';
|
|
|
|
var CHAR = /[^ \t]/;
|
|
var MULTIVALUE_DELIMITER = ',';
|
|
var VALUE_DELIMITER = ':';
|
|
var PARAM_DELIMITER = ';';
|
|
var PARAM_NAME_DELIMITER = '=';
|
|
var DEFAULT_TYPE = 'text';
|
|
|
|
var design = ICAL.design;
|
|
var helpers = ICAL.helpers;
|
|
|
|
function ParserError(message) {
|
|
this.message = message;
|
|
|
|
try {
|
|
throw new Error();
|
|
} catch (e) {
|
|
var split = e.stack.split('\n');
|
|
split.shift();
|
|
this.stack = split.join('\n');
|
|
}
|
|
}
|
|
|
|
ParserError.prototype = {
|
|
__proto__: Error.prototype
|
|
};
|
|
|
|
function parser(input) {
|
|
var state = {};
|
|
var root = state.component = [
|
|
'icalendar'
|
|
];
|
|
|
|
state.stack = [root];
|
|
|
|
parser._eachLine(input, function(err, line) {
|
|
parser._handleContentLine(line, state);
|
|
});
|
|
|
|
|
|
// when there are still items on the stack
|
|
// throw a fatal error, a component was not closed
|
|
// correctly in that case.
|
|
if (state.stack.length > 1) {
|
|
throw new ParserError(
|
|
'invalid ical body. component began but did not end'
|
|
);
|
|
}
|
|
|
|
state = null;
|
|
|
|
return root;
|
|
}
|
|
|
|
// classes & constants
|
|
parser.ParserError = ParserError;
|
|
|
|
parser._formatName = function(name) {
|
|
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 lastParamIndex;
|
|
var lastValuePos;
|
|
|
|
// name of property or begin/end
|
|
var name;
|
|
var value;
|
|
// 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) {
|
|
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 = line.substring(0, valuePos).toLowerCase();
|
|
value = line.substring(valuePos + 1);
|
|
|
|
if (name === 'begin') {
|
|
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;
|
|
} 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
|
|
* is sane and its unlikely that we can serialize
|
|
* the result correctly either.
|
|
*/
|
|
throw new ParserError(
|
|
'invalid line (no token ";" or ":") "' + line + '"'
|
|
);
|
|
}
|
|
|
|
var valueType;
|
|
var multiValue = false;
|
|
var propertyDetails;
|
|
|
|
if (name in design.property) {
|
|
propertyDetails = design.property[name];
|
|
|
|
if ('multiValue' in propertyDetails) {
|
|
multiValue = propertyDetails.multiValue;
|
|
}
|
|
|
|
if (value && 'detectType' in propertyDetails) {
|
|
valueType = propertyDetails.detectType(value);
|
|
}
|
|
}
|
|
|
|
// attempt to determine value
|
|
if (!valueType) {
|
|
if (!('value' in params)) {
|
|
if (propertyDetails) {
|
|
valueType = propertyDetails.defaultType;
|
|
} else {
|
|
valueType = DEFAULT_TYPE;
|
|
}
|
|
} else {
|
|
// possible to avoid this?
|
|
valueType = params.value.toLowerCase();
|
|
}
|
|
}
|
|
|
|
delete params.value;
|
|
|
|
/**
|
|
* Note on `var result` juggling:
|
|
*
|
|
* I observed that building the array in pieces has adverse
|
|
* effects on performance, so where possible we inline the creation.
|
|
* Its a little ugly but resulted in ~2000 additional ops/sec.
|
|
*/
|
|
|
|
if (value) {
|
|
if (multiValue) {
|
|
var result = [name, params, valueType];
|
|
parser._parseMultiValue(value, multiValue, valueType, result);
|
|
} else {
|
|
value = parser._parseValue(value, valueType);
|
|
var result = [name, params, valueType, value];
|
|
}
|
|
} else {
|
|
var result = [name, params, valueType];
|
|
}
|
|
|
|
state.component[1].push(result);
|
|
};
|
|
|
|
/**
|
|
* @param {String} value original value.
|
|
* @param {String} type type of value.
|
|
* @return {Object} varies on type.
|
|
*/
|
|
parser._parseValue = function(value, type) {
|
|
if (type in design.value && 'fromICAL' in design.value[type]) {
|
|
return design.value[type].fromICAL(value);
|
|
}
|
|
return value;
|
|
};
|
|
|
|
/**
|
|
* Parse parameters from a string to object.
|
|
*
|
|
* @param {String} line a single unfolded line.
|
|
* @param {Numeric} start position to start looking for properties.
|
|
* @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) {
|
|
|
|
name = line.substr(lastParam + 1, pos - lastParam - 1);
|
|
|
|
var nextChar = line[pos + 1];
|
|
if (nextChar === '"') {
|
|
var valuePos = pos + 2;
|
|
pos = helpers.unescapedIndexOf(line, '"', valuePos);
|
|
value = line.substr(valuePos, pos - valuePos);
|
|
lastParam = helpers.unescapedIndexOf(line, PARAM_DELIMITER, pos);
|
|
} else {
|
|
var valuePos = pos + 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);
|
|
|
|
if (nextPos === -1) {
|
|
nextPos = line.length;
|
|
}
|
|
pos = false;
|
|
} else {
|
|
lastParam = nextPos;
|
|
}
|
|
|
|
value = line.substr(valuePos, nextPos - valuePos);
|
|
}
|
|
|
|
if (name in design.param && design.param[name].valueType) {
|
|
type = design.param[name].valueType;
|
|
} else {
|
|
type = DEFAULT_TYPE;
|
|
}
|
|
|
|
result[name.toLowerCase()] = parser._parseValue(value, type);
|
|
}
|
|
return [result, value, valuePos];
|
|
}
|
|
|
|
/**
|
|
* Parse a multi value string
|
|
*/
|
|
parser._parseMultiValue = function(buffer, delim, type, result) {
|
|
var pos = 0;
|
|
var lastPos = 0;
|
|
|
|
// split each piece
|
|
while ((pos = helpers.unescapedIndexOf(buffer, delim, lastPos)) !== -1) {
|
|
var value = buffer.substr(lastPos, pos - lastPos);
|
|
result.push(parser._parseValue(value, type));
|
|
lastPos = pos + 1;
|
|
}
|
|
|
|
// on the last piece take the rest of string
|
|
result.push(
|
|
parser._parseValue(buffer.substr(lastPos), type)
|
|
);
|
|
|
|
return result;
|
|
}
|
|
|
|
parser._eachLine = function(buffer, callback) {
|
|
var len = buffer.length;
|
|
var lastPos = buffer.search(CHAR);
|
|
var pos = lastPos;
|
|
var line;
|
|
var firstChar;
|
|
|
|
var newlineOffset;
|
|
|
|
do {
|
|
pos = buffer.indexOf('\n', lastPos) + 1;
|
|
|
|
if (buffer[pos - 2] === '\r') {
|
|
newlineOffset = 2;
|
|
} else {
|
|
newlineOffset = 1;
|
|
}
|
|
|
|
if (pos === 0) {
|
|
pos = len;
|
|
newlineOffset = 0;
|
|
}
|
|
|
|
firstChar = buffer[lastPos];
|
|
|
|
if (firstChar === ' ' || firstChar === '\t') {
|
|
// add to line
|
|
line += buffer.substr(
|
|
lastPos + 1,
|
|
pos - lastPos - (newlineOffset + 1)
|
|
);
|
|
} else {
|
|
if (line)
|
|
callback(null, line);
|
|
// push line
|
|
line = buffer.substr(
|
|
lastPos,
|
|
pos - lastPos - newlineOffset
|
|
);
|
|
}
|
|
|
|
lastPos = pos;
|
|
} while (pos !== len);
|
|
|
|
// extra ending line
|
|
line = line.trim();
|
|
|
|
if (line.length)
|
|
callback(null, line);
|
|
}
|
|
|
|
return parser;
|
|
|
|
}());
|
|
ICAL.Component = (function() {
|
|
'use strict';
|
|
|
|
var PROPERTY_INDEX = 1;
|
|
var COMPONENT_INDEX = 2;
|
|
var NAME_INDEX = 0;
|
|
|
|
/**
|
|
* Create a wrapper for a jCal component.
|
|
*
|
|
* @param {Array|String} jCal
|
|
* raw jCal component data OR name of new component.
|
|
* @param {ICAL.Component} parent parent component to associate.
|
|
*/
|
|
function Component(jCal, parent) {
|
|
if (typeof(jCal) === 'string') {
|
|
// jCal spec (name, properties, components)
|
|
jCal = [jCal, [], []];
|
|
}
|
|
|
|
// mostly for legacy reasons.
|
|
this.jCal = jCal;
|
|
|
|
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
|
|
* count here.
|
|
*/
|
|
_hydratedPropertyCount: 0,
|
|
|
|
/**
|
|
* The same count as for _hydratedPropertyCount, but for subcomponents
|
|
*/
|
|
_hydratedComponentCount: 0,
|
|
|
|
get name() {
|
|
return this.jCal[NAME_INDEX];
|
|
},
|
|
|
|
_hydrateComponent: function(index) {
|
|
if (!this._components) {
|
|
this._components = [];
|
|
this._hydratedComponentCount = 0;
|
|
}
|
|
|
|
if (this._components[index]) {
|
|
return this._components[index];
|
|
}
|
|
|
|
var comp = new Component(
|
|
this.jCal[COMPONENT_INDEX][index],
|
|
this
|
|
);
|
|
|
|
this._hydratedComponentCount++;
|
|
return this._components[index] = comp;
|
|
},
|
|
|
|
_hydrateProperty: function(index) {
|
|
if (!this._properties) {
|
|
this._properties = [];
|
|
this._hydratedPropertyCount = 0;
|
|
}
|
|
|
|
if (this._properties[index]) {
|
|
return this._properties[index];
|
|
}
|
|
|
|
var prop = new ICAL.Property(
|
|
this.jCal[PROPERTY_INDEX][index],
|
|
this
|
|
);
|
|
|
|
this._hydratedPropertyCount++;
|
|
return this._properties[index] = prop;
|
|
},
|
|
|
|
/**
|
|
* Finds first sub component, optionally filtered by name.
|
|
*
|
|
* @method getFirstSubcomponent
|
|
* @param {String} [name] optional name to filter by.
|
|
*/
|
|
getFirstSubcomponent: function(name) {
|
|
if (name) {
|
|
var i = 0;
|
|
var comps = this.jCal[COMPONENT_INDEX];
|
|
var len = comps.length;
|
|
|
|
for (; i < len; i++) {
|
|
if (comps[i][NAME_INDEX] === name) {
|
|
var result = this._hydrateComponent(i);
|
|
return result;
|
|
}
|
|
}
|
|
} else {
|
|
if (this.jCal[COMPONENT_INDEX].length) {
|
|
return this._hydrateComponent(0);
|
|
}
|
|
}
|
|
|
|
// ensure we return a value (strict mode)
|
|
return null;
|
|
},
|
|
|
|
/**
|
|
* Finds all sub components, optionally filtering by name.
|
|
*
|
|
* @method getAllSubcomponents
|
|
* @param {String} [name] optional name to filter by.
|
|
*/
|
|
getAllSubcomponents: function(name) {
|
|
var jCalLen = this.jCal[COMPONENT_INDEX].length;
|
|
|
|
if (name) {
|
|
var comps = this.jCal[COMPONENT_INDEX];
|
|
var result = [];
|
|
var i = 0;
|
|
|
|
for (; i < jCalLen; i++) {
|
|
if (name === comps[i][NAME_INDEX]) {
|
|
result.push(
|
|
this._hydrateComponent(i)
|
|
);
|
|
}
|
|
}
|
|
return result;
|
|
} else {
|
|
if (!this._components ||
|
|
(this._hydratedComponentCount !== jCalLen)) {
|
|
var i = 0;
|
|
for (; i < jCalLen; i++) {
|
|
this._hydrateComponent(i);
|
|
}
|
|
}
|
|
|
|
return this._components;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Returns true when a named property exists.
|
|
*
|
|
* @param {String} name property name.
|
|
* @return {Boolean} true when property is found.
|
|
*/
|
|
hasProperty: function(name) {
|
|
var props = this.jCal[PROPERTY_INDEX];
|
|
var len = props.length;
|
|
|
|
var i = 0;
|
|
for (; i < len; i++) {
|
|
// 0 is property name
|
|
if (props[i][NAME_INDEX] === name) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
},
|
|
|
|
/**
|
|
* Finds first property.
|
|
*
|
|
* @param {String} [name] lowercase name of property.
|
|
* @return {ICAL.Property} found property.
|
|
*/
|
|
getFirstProperty: function(name) {
|
|
if (name) {
|
|
var i = 0;
|
|
var props = this.jCal[PROPERTY_INDEX];
|
|
var len = props.length;
|
|
|
|
for (; i < len; i++) {
|
|
if (props[i][NAME_INDEX] === name) {
|
|
var result = this._hydrateProperty(i);
|
|
return result;
|
|
}
|
|
}
|
|
} else {
|
|
if (this.jCal[PROPERTY_INDEX].length) {
|
|
return this._hydrateProperty(0);
|
|
}
|
|
}
|
|
|
|
return null;
|
|
},
|
|
|
|
/**
|
|
* Returns first properties value if available.
|
|
*
|
|
* @param {String} [name] (lowecase) property name.
|
|
* @return {String} property value.
|
|
*/
|
|
getFirstPropertyValue: function(name) {
|
|
var prop = this.getFirstProperty(name);
|
|
if (prop) {
|
|
return prop.getFirstValue();
|
|
}
|
|
|
|
return null;
|
|
},
|
|
|
|
/**
|
|
* get all properties in the component.
|
|
*
|
|
* @param {String} [name] (lowercase) property name.
|
|
* @return {Array[ICAL.Property]} list of properties.
|
|
*/
|
|
getAllProperties: function(name) {
|
|
var jCalLen = this.jCal[PROPERTY_INDEX].length;
|
|
|
|
if (name) {
|
|
var props = this.jCal[PROPERTY_INDEX];
|
|
var result = [];
|
|
var i = 0;
|
|
|
|
for (; i < jCalLen; i++) {
|
|
if (name === props[i][NAME_INDEX]) {
|
|
result.push(
|
|
this._hydrateProperty(i)
|
|
);
|
|
}
|
|
}
|
|
return result;
|
|
} else {
|
|
if (!this._properties ||
|
|
(this._hydratedPropertyCount !== jCalLen)) {
|
|
var i = 0;
|
|
for (; i < jCalLen; i++) {
|
|
this._hydrateProperty(i);
|
|
}
|
|
}
|
|
|
|
return this._properties;
|
|
}
|
|
|
|
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) {
|
|
var i = 0;
|
|
var objects = this.jCal[jCalIndex];
|
|
var len = objects.length;
|
|
var cached = this[cache];
|
|
|
|
if (typeof(nameOrObject) === 'string') {
|
|
for (; i < len; i++) {
|
|
if (objects[i][NAME_INDEX] === nameOrObject) {
|
|
this._removeObjectByIndex(jCalIndex, cached, i);
|
|
return true;
|
|
}
|
|
}
|
|
} else if (cached) {
|
|
for (; i < len; i++) {
|
|
if (cached[i] && cached[i] === nameOrObject) {
|
|
this._removeObjectByIndex(jCalIndex, cached, i);
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
},
|
|
|
|
_removeAllObjects: function(jCalIndex, cache, name) {
|
|
var cached = this[cache];
|
|
|
|
// 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);
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* 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
|
|
* the instance of a specific component.
|
|
*
|
|
* @param {ICAL.Component|String} nameOrComp comp type.
|
|
* @return {Boolean} true when comp is removed.
|
|
*/
|
|
removeSubcomponent: function(nameOrComp) {
|
|
var removed = this._removeObject(COMPONENT_INDEX, '_components', nameOrComp);
|
|
if (removed) {
|
|
this._hydratedComponentCount--;
|
|
}
|
|
return removed;
|
|
},
|
|
|
|
/**
|
|
* Removes all components or (if given) all
|
|
* components by a particular name.
|
|
*
|
|
* @param {String} [name] (lowercase) component name.
|
|
*/
|
|
removeAllSubcomponents: function(name) {
|
|
var removed = this._removeAllObjects(COMPONENT_INDEX, '_components', name);
|
|
this._hydratedComponentCount = 0;
|
|
return removed;
|
|
},
|
|
|
|
/**
|
|
* Adds a property to the component.
|
|
*
|
|
* @param {ICAL.Property} property object.
|
|
*/
|
|
addProperty: function(property) {
|
|
if (!(property instanceof ICAL.Property)) {
|
|
throw new TypeError('must instance of ICAL.Property');
|
|
}
|
|
|
|
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);
|
|
prop.setValue(value);
|
|
|
|
this.addProperty(prop);
|
|
|
|
return prop;
|
|
},
|
|
|
|
/**
|
|
* Helper method that will update or create a property
|
|
* of the given name and sets its value.
|
|
*
|
|
* @param {String} name property name.
|
|
* @param {Object} value property value.
|
|
* @return {ICAL.Property} property.
|
|
*/
|
|
updatePropertyWithValue: function(name, value) {
|
|
var prop = this.getFirstProperty(name);
|
|
|
|
if (prop) {
|
|
prop.setValue(value);
|
|
} else {
|
|
prop = this.addPropertyWithValue(name, value);
|
|
}
|
|
|
|
return prop;
|
|
},
|
|
|
|
/**
|
|
* Removes a single property by name or
|
|
* the instance of the specific property.
|
|
*
|
|
* @param {String|ICAL.Property} nameOrProp to remove.
|
|
* @return {Boolean} true when deleted.
|
|
*/
|
|
removeProperty: function(nameOrProp) {
|
|
var removed = this._removeObject(PROPERTY_INDEX, '_properties', nameOrProp);
|
|
if (removed) {
|
|
this._hydratedPropertyCount--;
|
|
}
|
|
return removed;
|
|
},
|
|
|
|
/**
|
|
* Removes all properties associated with this component.
|
|
*
|
|
* @param {String} [name] (lowecase) optional property name.
|
|
*/
|
|
removeAllProperties: function(name) {
|
|
var removed = this._removeAllObjects(PROPERTY_INDEX, '_properties', name);
|
|
this._hydratedPropertyCount = 0;
|
|
return removed;
|
|
},
|
|
|
|
toJSON: function() {
|
|
return this.jCal;
|
|
},
|
|
|
|
toString: function() {
|
|
return ICAL.stringify.component(
|
|
this.jCal
|
|
);
|
|
}
|
|
|
|
};
|
|
|
|
return Component;
|
|
|
|
}());
|
|
ICAL.Property = (function() {
|
|
'use strict';
|
|
|
|
var NAME_INDEX = 0;
|
|
var PROP_INDEX = 1;
|
|
var TYPE_INDEX = 2;
|
|
var VALUE_INDEX = 3;
|
|
|
|
var design = ICAL.design;
|
|
|
|
/**
|
|
* Provides a nicer interface to any kind of property.
|
|
* Its important to note that mutations done in the wrapper
|
|
* directly effect (mutate) the jCal object used to initialize.
|
|
*
|
|
* 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} [parent] parent 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) {
|
|
var type = prop.defaultType;
|
|
} else {
|
|
var type = design.defaultType;
|
|
}
|
|
} else {
|
|
var type = design.defaultType;
|
|
}
|
|
|
|
jCal = [name, {}, type];
|
|
}
|
|
|
|
this.jCal = jCal;
|
|
this.parent = parent || null;
|
|
this._updateType();
|
|
}
|
|
|
|
Property.prototype = {
|
|
get type() {
|
|
return this.jCal[TYPE_INDEX];
|
|
},
|
|
|
|
get name() {
|
|
return this.jCal[NAME_INDEX];
|
|
},
|
|
|
|
_updateType: function() {
|
|
if (this.type in design.value) {
|
|
var designType = design.value[this.type];
|
|
|
|
if ('decorate' in design.value[this.type]) {
|
|
this.isDecorated = true;
|
|
} else {
|
|
this.isDecorated = false;
|
|
}
|
|
|
|
if (this.name in design.property) {
|
|
if ('multiValue' in design.property[this.name]) {
|
|
this.isMultiValue = true;
|
|
} else {
|
|
this.isMultiValue = false;
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Hydrate a single value.
|
|
*/
|
|
_hydrateValue: function(index) {
|
|
if (this._values && this._values[index]) {
|
|
return this._values[index];
|
|
}
|
|
|
|
// for the case where there is no value.
|
|
if (this.jCal.length <= (VALUE_INDEX + index)) {
|
|
return null;
|
|
}
|
|
|
|
if (this.isDecorated) {
|
|
if (!this._values) {
|
|
this._values = [];
|
|
}
|
|
return this._values[index] = this._decorate(
|
|
this.jCal[VALUE_INDEX + index]
|
|
);
|
|
} else {
|
|
return this.jCal[VALUE_INDEX + index];
|
|
}
|
|
},
|
|
|
|
_decorate: function(value) {
|
|
return design.value[this.type].decorate(value, this);
|
|
},
|
|
|
|
_undecorate: function(value) {
|
|
return design.value[this.type].undecorate(value, this);
|
|
},
|
|
|
|
_setDecoratedValue: function(value, index) {
|
|
if (!this._values) {
|
|
this._values = [];
|
|
}
|
|
|
|
if (typeof(value) === 'object' && 'icaltype' in value) {
|
|
// decorated value
|
|
this.jCal[VALUE_INDEX + index] = this._undecorate(value);
|
|
this._values[index] = value;
|
|
} else {
|
|
// undecorated value
|
|
this.jCal[VALUE_INDEX + index] = value;
|
|
this._values[index] = this._decorate(value);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Gets a param on the property.
|
|
*
|
|
* @param {String} name prop name (lowercase).
|
|
* @return {String} prop value.
|
|
*/
|
|
getParameter: function(name) {
|
|
return this.jCal[PROP_INDEX][name];
|
|
},
|
|
|
|
/**
|
|
* Sets a param on the property.
|
|
*
|
|
* @param {String} value property value.
|
|
*/
|
|
setParameter: function(name, value) {
|
|
this.jCal[PROP_INDEX][name] = value;
|
|
},
|
|
|
|
/**
|
|
* Removes a parameter
|
|
*
|
|
* @param {String} name prop name (lowercase).
|
|
*/
|
|
removeParameter: function(name) {
|
|
return delete this.jCal[PROP_INDEX][name];
|
|
},
|
|
|
|
/**
|
|
* Get the default type based on this property's name.
|
|
*
|
|
* @return {String} the default type for this property.
|
|
*/
|
|
getDefaultType: function() {
|
|
var name = this.name
|
|
if (name in design.property) {
|
|
var details = design.property[name];
|
|
if ('defaultType' in details) {
|
|
return details.defaultType;
|
|
}
|
|
}
|
|
return null;
|
|
},
|
|
|
|
/**
|
|
* Sets type of property and clears out any
|
|
* existing values of the current type.
|
|
*
|
|
* @param {String} type new iCAL type (see design.values).
|
|
*/
|
|
resetType: function(type) {
|
|
this.removeAllValues();
|
|
this.jCal[TYPE_INDEX] = type;
|
|
this._updateType();
|
|
},
|
|
|
|
/**
|
|
* Finds first property value.
|
|
*
|
|
* @return {String} first property value.
|
|
*/
|
|
getFirstValue: function() {
|
|
return this._hydrateValue(0);
|
|
},
|
|
|
|
/**
|
|
* Gets all values on the property.
|
|
*
|
|
* NOTE: this creates an array during each call.
|
|
*
|
|
* @return {Array} list of values.
|
|
*/
|
|
getValues: function() {
|
|
var len = this.jCal.length - VALUE_INDEX;
|
|
|
|
if (len < 1) {
|
|
// its possible for a property to have no value.
|
|
return [];
|
|
}
|
|
|
|
var i = 0;
|
|
var result = [];
|
|
|
|
for (; i < len; i++) {
|
|
result[i] = this._hydrateValue(i);
|
|
}
|
|
|
|
return result;
|
|
},
|
|
|
|
removeAllValues: function() {
|
|
if (this._values) {
|
|
this._values.length = 0;
|
|
}
|
|
this.jCal.length = 3;
|
|
},
|
|
|
|
/**
|
|
* Sets the values of the property.
|
|
* Will overwrite the existing values.
|
|
*
|
|
* @param {Array} values an array of values.
|
|
*/
|
|
setValues: function(values) {
|
|
if (!this.isMultiValue) {
|
|
throw new Error(
|
|
this.name + ': does not not support mulitValue.\n' +
|
|
'override isMultiValue'
|
|
);
|
|
}
|
|
|
|
var len = values.length;
|
|
var i = 0;
|
|
this.removeAllValues();
|
|
|
|
if (len > 0 &&
|
|
typeof(values[0]) === 'object' &&
|
|
'icaltype' in values[0]) {
|
|
this.resetType(values[0].icaltype);
|
|
}
|
|
|
|
if (this.isDecorated) {
|
|
for (; i < len; i++) {
|
|
this._setDecoratedValue(values[i], i);
|
|
}
|
|
} else {
|
|
for (; i < len; i++) {
|
|
this.jCal[VALUE_INDEX + i] = values[i];
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* 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);
|
|
}
|
|
|
|
if (this.isDecorated) {
|
|
this._setDecoratedValue(value, 0);
|
|
} else {
|
|
this.jCal[VALUE_INDEX] = value;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Returns the jCal representation of this property.
|
|
*
|
|
* @return {Object} jCal.
|
|
*/
|
|
toJSON: function() {
|
|
return this.jCal;
|
|
},
|
|
|
|
toICAL: function() {
|
|
return ICAL.stringify.property(
|
|
this.jCal
|
|
);
|
|
}
|
|
|
|
};
|
|
|
|
return Property;
|
|
|
|
}());
|
|
ICAL.UtcOffset = (function() {
|
|
|
|
function UtcOffset(aData) {
|
|
this.hours = aData.hours;
|
|
this.minutes = aData.minutes;
|
|
this.factor = aData.factor;
|
|
};
|
|
|
|
UtcOffset.prototype = {
|
|
|
|
hours: null,
|
|
minutes: null,
|
|
factor: null,
|
|
|
|
icaltype: "utc-offset",
|
|
|
|
toString: function toString() {
|
|
return (this.factor == 1 ? "+" : "-") +
|
|
ICAL.helpers.pad2(this.hours) + ':' +
|
|
ICAL.helpers.pad2(this.minutes);
|
|
}
|
|
};
|
|
|
|
UtcOffset.fromString = function(aString) {
|
|
// -05:00
|
|
var options = {};
|
|
//TODO: support seconds per rfc5545 ?
|
|
options.factor = (aString[0] === '+') ? 1 : -1;
|
|
options.hours = ICAL.helpers.strictParseInt(aString.substr(1, 2));
|
|
options.minutes = ICAL.helpers.strictParseInt(aString.substr(4, 2));
|
|
|
|
return new ICAL.UtcOffset(options);
|
|
};
|
|
|
|
|
|
return UtcOffset;
|
|
|
|
}());
|
|
ICAL.Binary = (function() {
|
|
|
|
function Binary(aValue) {
|
|
this.value = aValue;
|
|
};
|
|
|
|
Binary.prototype = {
|
|
icaltype: "binary",
|
|
|
|
decodeValue: function decodeValue() {
|
|
return this._b64_decode(this.value);
|
|
},
|
|
|
|
setEncodedValue: function setEncodedValue(val) {
|
|
this.value = this._b64_encode(val);
|
|
},
|
|
|
|
_b64_encode: function base64_encode(data) {
|
|
// http://kevin.vanzonneveld.net
|
|
// + original by: Tyler Akins (http://rumkin.com)
|
|
// + improved by: Bayron Guevara
|
|
// + improved by: Thunder.m
|
|
// + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
|
|
// + bugfixed by: Pellentesque Malesuada
|
|
// + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
|
|
// + improved by: Rafał Kukawski (http://kukawski.pl)
|
|
// * example 1: base64_encode('Kevin van Zonneveld');
|
|
// * returns 1: 'S2V2aW4gdmFuIFpvbm5ldmVsZA=='
|
|
// mozilla has this native
|
|
// - but breaks in 2.0.0.12!
|
|
//if (typeof this.window['atob'] == 'function') {
|
|
// return atob(data);
|
|
//}
|
|
var b64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" +
|
|
"abcdefghijklmnopqrstuvwxyz0123456789+/=";
|
|
var o1, o2, o3, h1, h2, h3, h4, bits, i = 0,
|
|
ac = 0,
|
|
enc = "",
|
|
tmp_arr = [];
|
|
|
|
if (!data) {
|
|
return data;
|
|
}
|
|
|
|
do { // pack three octets into four hexets
|
|
o1 = data.charCodeAt(i++);
|
|
o2 = data.charCodeAt(i++);
|
|
o3 = data.charCodeAt(i++);
|
|
|
|
bits = o1 << 16 | o2 << 8 | o3;
|
|
|
|
h1 = bits >> 18 & 0x3f;
|
|
h2 = bits >> 12 & 0x3f;
|
|
h3 = bits >> 6 & 0x3f;
|
|
h4 = bits & 0x3f;
|
|
|
|
// use hexets to index into b64, and append result to encoded string
|
|
tmp_arr[ac++] = b64.charAt(h1) + b64.charAt(h2) + b64.charAt(h3) + b64.charAt(h4);
|
|
} while (i < data.length);
|
|
|
|
enc = tmp_arr.join('');
|
|
|
|
var r = data.length % 3;
|
|
|
|
return (r ? enc.slice(0, r - 3) : enc) + '==='.slice(r || 3);
|
|
|
|
},
|
|
|
|
_b64_decode: function base64_decode(data) {
|
|
// http://kevin.vanzonneveld.net
|
|
// + original by: Tyler Akins (http://rumkin.com)
|
|
// + improved by: Thunder.m
|
|
// + input by: Aman Gupta
|
|
// + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
|
|
// + bugfixed by: Onno Marsman
|
|
// + bugfixed by: Pellentesque Malesuada
|
|
// + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
|
|
// + input by: Brett Zamir (http://brett-zamir.me)
|
|
// + bugfixed by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
|
|
// * example 1: base64_decode('S2V2aW4gdmFuIFpvbm5ldmVsZA==');
|
|
// * returns 1: 'Kevin van Zonneveld'
|
|
// mozilla has this native
|
|
// - but breaks in 2.0.0.12!
|
|
//if (typeof this.window['btoa'] == 'function') {
|
|
// return btoa(data);
|
|
//}
|
|
var b64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" +
|
|
"abcdefghijklmnopqrstuvwxyz0123456789+/=";
|
|
var o1, o2, o3, h1, h2, h3, h4, bits, i = 0,
|
|
ac = 0,
|
|
dec = "",
|
|
tmp_arr = [];
|
|
|
|
if (!data) {
|
|
return data;
|
|
}
|
|
|
|
data += '';
|
|
|
|
do { // unpack four hexets into three octets using index points in b64
|
|
h1 = b64.indexOf(data.charAt(i++));
|
|
h2 = b64.indexOf(data.charAt(i++));
|
|
h3 = b64.indexOf(data.charAt(i++));
|
|
h4 = b64.indexOf(data.charAt(i++));
|
|
|
|
bits = h1 << 18 | h2 << 12 | h3 << 6 | h4;
|
|
|
|
o1 = bits >> 16 & 0xff;
|
|
o2 = bits >> 8 & 0xff;
|
|
o3 = bits & 0xff;
|
|
|
|
if (h3 == 64) {
|
|
tmp_arr[ac++] = String.fromCharCode(o1);
|
|
} else if (h4 == 64) {
|
|
tmp_arr[ac++] = String.fromCharCode(o1, o2);
|
|
} else {
|
|
tmp_arr[ac++] = String.fromCharCode(o1, o2, o3);
|
|
}
|
|
} while (i < data.length);
|
|
|
|
dec = tmp_arr.join('');
|
|
|
|
return dec;
|
|
},
|
|
|
|
toString: function() {
|
|
return this.value;
|
|
}
|
|
};
|
|
|
|
Binary.fromString = function(aString) {
|
|
return new Binary(aString);
|
|
}
|
|
|
|
return Binary;
|
|
|
|
}());
|
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
* Portions Copyright (C) Philipp Kewisch, 2011-2012 */
|
|
|
|
|
|
|
|
(typeof(ICAL) === 'undefined')? ICAL = {} : '';
|
|
(function() {
|
|
ICAL.Period = function icalperiod(aData) {
|
|
this.wrappedJSObject = this;
|
|
|
|
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 && 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;
|
|
}
|
|
|
|
if (aData && 'duration' in aData) {
|
|
if (aData.duration && !(aData.duration instanceof ICAL.Duration)) {
|
|
throw new TypeError('.duration must be an instance of ICAL.Duration');
|
|
}
|
|
this.duration = aData.duration;
|
|
}
|
|
};
|
|
|
|
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);
|
|
}
|
|
},
|
|
|
|
getEnd: function() {
|
|
if (this.end) {
|
|
return this.end;
|
|
} else {
|
|
var end = this.start.clone();
|
|
end.addDuration(this.duration);
|
|
return end;
|
|
}
|
|
},
|
|
|
|
toString: function toString() {
|
|
return this.start + "/" + (this.end || this.duration);
|
|
},
|
|
|
|
toICALString: function() {
|
|
return this.start.toICALString() + "/" +
|
|
(this.end || this.duration).toICALString();
|
|
}
|
|
};
|
|
|
|
ICAL.Period.fromString = function fromString(str, prop) {
|
|
var parts = str.split('/');
|
|
|
|
if (parts.length !== 2) {
|
|
throw new Error(
|
|
'Invalid string value: "' + str + '" must contain a "/" char.'
|
|
);
|
|
}
|
|
|
|
var options = {
|
|
start: ICAL.Time.fromDateTimeString(parts[0], prop)
|
|
};
|
|
|
|
var end = parts[1];
|
|
|
|
if (ICAL.Duration.isValueString(end)) {
|
|
options.duration = ICAL.Duration.fromString(end);
|
|
} else {
|
|
options.end = ICAL.Time.fromDateTimeString(end, prop);
|
|
}
|
|
|
|
return new ICAL.Period(options);
|
|
};
|
|
|
|
ICAL.Period.fromData = function fromData(aData) {
|
|
return new ICAL.Period(aData);
|
|
};
|
|
|
|
})();
|
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
* Portions Copyright (C) Philipp Kewisch, 2011-2012 */
|
|
|
|
|
|
|
|
(typeof(ICAL) === 'undefined')? ICAL = {} : '';
|
|
(function() {
|
|
var DURATION_LETTERS = /([PDWHMTS]{1,1})/;
|
|
|
|
ICAL.Duration = function icalduration(data) {
|
|
this.wrappedJSObject = this;
|
|
this.fromData(data);
|
|
};
|
|
|
|
ICAL.Duration.prototype = {
|
|
|
|
weeks: 0,
|
|
days: 0,
|
|
hours: 0,
|
|
minutes: 0,
|
|
seconds: 0,
|
|
isNegative: false,
|
|
icalclass: "icalduration",
|
|
icaltype: "duration",
|
|
|
|
clone: function clone() {
|
|
return ICAL.Duration.fromData(this);
|
|
},
|
|
|
|
toSeconds: function toSeconds() {
|
|
var seconds = this.seconds + 60 * this.minutes + 3600 * this.hours +
|
|
86400 * this.days + 7 * 86400 * this.weeks;
|
|
return (this.isNegative ? -seconds : seconds);
|
|
},
|
|
|
|
fromSeconds: function fromSeconds(aSeconds) {
|
|
var secs = Math.abs(aSeconds);
|
|
|
|
this.isNegative = (aSeconds < 0);
|
|
this.days = ICAL.helpers.trunc(secs / 86400);
|
|
|
|
// If we have a flat number of weeks, use them.
|
|
if (this.days % 7 == 0) {
|
|
this.weeks = this.days / 7;
|
|
this.days = 0;
|
|
} else {
|
|
this.weeks = 0;
|
|
}
|
|
|
|
secs -= (this.days + 7 * this.weeks) * 86400;
|
|
|
|
this.hours = ICAL.helpers.trunc(secs / 3600);
|
|
secs -= this.hours * 3600;
|
|
|
|
this.minutes = ICAL.helpers.trunc(secs / 60);
|
|
secs -= this.minutes * 60;
|
|
|
|
this.seconds = secs;
|
|
return this;
|
|
},
|
|
|
|
fromData: function fromData(aData) {
|
|
var propsToCopy = ["weeks", "days", "hours",
|
|
"minutes", "seconds", "isNegative"];
|
|
for (var key in propsToCopy) {
|
|
var prop = propsToCopy[key];
|
|
if (aData && prop in aData) {
|
|
this[prop] = aData[prop];
|
|
} else {
|
|
this[prop] = 0;
|
|
}
|
|
}
|
|
},
|
|
|
|
reset: function reset() {
|
|
this.isNegative = false;
|
|
this.weeks = 0;
|
|
this.days = 0;
|
|
this.hours = 0;
|
|
this.minutes = 0;
|
|
this.seconds = 0;
|
|
},
|
|
|
|
compare: function compare(aOther) {
|
|
var thisSeconds = this.toSeconds();
|
|
var otherSeconds = aOther.toSeconds();
|
|
return (thisSeconds > otherSeconds) - (thisSeconds < otherSeconds);
|
|
},
|
|
|
|
normalize: function normalize() {
|
|
this.fromSeconds(this.toSeconds());
|
|
return this;
|
|
},
|
|
|
|
toString: function toString() {
|
|
if (this.toSeconds() == 0) {
|
|
return "PT0S";
|
|
} else {
|
|
var str = "";
|
|
if (this.isNegative) str += "-";
|
|
str += "P";
|
|
if (this.weeks) str += this.weeks + "W";
|
|
if (this.days) str += this.days + "D";
|
|
|
|
if (this.hours || this.minutes || this.seconds) {
|
|
str += "T";
|
|
if (this.hours) str += this.hours + "H";
|
|
if (this.minutes) str += this.minutes + "M";
|
|
if (this.seconds) str += this.seconds + "S";
|
|
}
|
|
return str;
|
|
}
|
|
},
|
|
|
|
toICALString: function() {
|
|
return this.toString();
|
|
}
|
|
};
|
|
|
|
ICAL.Duration.fromSeconds = function icalduration_from_seconds(aSeconds) {
|
|
return (new ICAL.Duration()).fromSeconds(aSeconds);
|
|
};
|
|
|
|
/**
|
|
* Internal helper function to handle a chunk of a duration.
|
|
*
|
|
* @param {String} letter type of duration chunk.
|
|
* @param {String} number numeric value or -/+.
|
|
* @param {Object} dict target to assign values to.
|
|
*/
|
|
function parseDurationChunk(letter, number, object) {
|
|
var type;
|
|
switch (letter) {
|
|
case 'P':
|
|
if (number && number === '-') {
|
|
object.isNegative = true;
|
|
} else {
|
|
object.isNegative = false;
|
|
}
|
|
// period
|
|
break;
|
|
case 'D':
|
|
type = 'days';
|
|
break;
|
|
case 'W':
|
|
type = 'weeks';
|
|
break;
|
|
case 'H':
|
|
type = 'hours';
|
|
break;
|
|
case 'M':
|
|
type = 'minutes';
|
|
break;
|
|
case 'S':
|
|
type = 'seconds';
|
|
break;
|
|
default:
|
|
// Not a valid chunk
|
|
return 0;
|
|
}
|
|
|
|
if (type) {
|
|
if (!number && number !== 0) {
|
|
throw new Error(
|
|
'invalid duration value: Missing number before "' + letter + '"'
|
|
);
|
|
}
|
|
var num = parseInt(number, 10);
|
|
if (ICAL.helpers.isStrictlyNaN(num)) {
|
|
throw new Error(
|
|
'invalid duration value: Invalid number "' + number + '" before "' + letter + '"'
|
|
);
|
|
}
|
|
object[type] = num;
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
/**
|
|
* @param {String} value raw ical value.
|
|
* @return {Boolean}
|
|
* true when the given value is of the duration ical type.
|
|
*/
|
|
ICAL.Duration.isValueString = function(string) {
|
|
return (string[0] === 'P' || string[1] === 'P');
|
|
},
|
|
|
|
ICAL.Duration.fromString = function icalduration_from_string(aStr) {
|
|
var pos = 0;
|
|
var dict = Object.create(null);
|
|
var chunks = 0;
|
|
|
|
while ((pos = aStr.search(DURATION_LETTERS)) !== -1) {
|
|
var type = aStr[pos];
|
|
var numeric = aStr.substr(0, pos);
|
|
aStr = aStr.substr(pos + 1);
|
|
|
|
chunks += parseDurationChunk(type, numeric, dict);
|
|
}
|
|
|
|
if (chunks < 2) {
|
|
// There must be at least a chunk with "P" and some unit chunk
|
|
throw new Error(
|
|
'invalid duration value: Not enough duration components in "' + aStr + '"'
|
|
);
|
|
}
|
|
|
|
return new ICAL.Duration(dict);
|
|
};
|
|
|
|
ICAL.Duration.fromData = function icalduration_from_data(aData) {
|
|
return new ICAL.Duration(aData);
|
|
};
|
|
})();
|
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
* Portions Copyright (C) Philipp Kewisch, 2011-2012 */
|
|
|
|
|
|
|
|
(typeof(ICAL) === 'undefined')? ICAL = {} : '';
|
|
(function() {
|
|
var OPTIONS = ["tzid", "location", "tznames",
|
|
"latitude", "longitude"];
|
|
|
|
/**
|
|
* Timezone representation, created by passing in a tzid and component.
|
|
*
|
|
* var vcalendar;
|
|
* var timezoneComp = vcalendar.getFirstSubcomponent('vtimezone');
|
|
* var tzid = timezoneComp.getFirstPropertyValue('tzid');
|
|
*
|
|
* var timezone = new ICAL.Timezone({
|
|
* component: timezoneComp,
|
|
* tzid
|
|
* });
|
|
*
|
|
*
|
|
* @param {Object} data options for class (see above).
|
|
*/
|
|
ICAL.Timezone = function icaltimezone(data) {
|
|
this.wrappedJSObject = this;
|
|
this.fromData(data);
|
|
};
|
|
|
|
ICAL.Timezone.prototype = {
|
|
|
|
tzid: "",
|
|
location: "",
|
|
tznames: "",
|
|
|
|
latitude: 0.0,
|
|
longitude: 0.0,
|
|
|
|
component: null,
|
|
|
|
expandedUntilYear: 0,
|
|
|
|
icalclass: "icaltimezone",
|
|
|
|
fromData: function fromData(aData) {
|
|
this.expandedUntilYear = 0;
|
|
this.changes = [];
|
|
|
|
if (aData instanceof ICAL.Component) {
|
|
// Either a component is passed directly
|
|
this.component = aData;
|
|
} else {
|
|
// Otherwise the component may be in the data object
|
|
if (aData && "component" in aData) {
|
|
if (typeof aData.component == "string") {
|
|
// If a string was passed, parse it as a component
|
|
var icalendar = ICAL.parse(aData.component);
|
|
this.component = new ICAL.Component(icalendar[1]);
|
|
} else if (aData.component instanceof ICAL.Component) {
|
|
// If it was a component already, then just set it
|
|
this.component = aData.component;
|
|
} else {
|
|
// Otherwise just null out the component
|
|
this.component = null;
|
|
}
|
|
}
|
|
|
|
// Copy remaining passed properties
|
|
for (var key in OPTIONS) {
|
|
var prop = OPTIONS[key];
|
|
if (aData && prop in aData) {
|
|
this[prop] = aData[prop];
|
|
}
|
|
}
|
|
}
|
|
|
|
// If we have a component but no TZID, attempt to get it from the
|
|
// component's properties.
|
|
if (this.component instanceof ICAL.Component && !this.tzid) {
|
|
this.tzid = this.component.getFirstPropertyValue('tzid');
|
|
}
|
|
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* Finds the utcOffset the given time would occur in this timezone.
|
|
*
|
|
* @return {Number} utc offset in seconds.
|
|
*/
|
|
utcOffset: function utcOffset(tt) {
|
|
if (this == ICAL.Timezone.utcTimezone || this == ICAL.Timezone.localTimezone) {
|
|
return 0;
|
|
}
|
|
|
|
this._ensureCoverage(tt.year);
|
|
|
|
if (!this.changes.length) {
|
|
return 0;
|
|
}
|
|
|
|
var tt_change = {
|
|
year: tt.year,
|
|
month: tt.month,
|
|
day: tt.day,
|
|
hour: tt.hour,
|
|
minute: tt.minute,
|
|
second: tt.second
|
|
};
|
|
|
|
var change_num = this._findNearbyChange(tt_change);
|
|
var change_num_to_use = -1;
|
|
var step = 1;
|
|
|
|
// TODO: replace with bin search?
|
|
for (;;) {
|
|
var change = ICAL.helpers.clone(this.changes[change_num], true);
|
|
if (change.utcOffset < change.prevUtcOffset) {
|
|
ICAL.Timezone.adjust_change(change, 0, 0, 0, change.utcOffset);
|
|
} else {
|
|
ICAL.Timezone.adjust_change(change, 0, 0, 0,
|
|
change.prevUtcOffset);
|
|
}
|
|
|
|
var cmp = ICAL.Timezone._compare_change_fn(tt_change, change);
|
|
|
|
if (cmp >= 0) {
|
|
change_num_to_use = change_num;
|
|
} else {
|
|
step = -1;
|
|
}
|
|
|
|
if (step == -1 && change_num_to_use != -1) {
|
|
break;
|
|
}
|
|
|
|
change_num += step;
|
|
|
|
if (change_num < 0) {
|
|
return 0;
|
|
}
|
|
|
|
if (change_num >= this.changes.length) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
var zone_change = this.changes[change_num_to_use];
|
|
var utcOffset_change = zone_change.utcOffset - zone_change.prevUtcOffset;
|
|
|
|
if (utcOffset_change < 0 && change_num_to_use > 0) {
|
|
var tmp_change = ICAL.helpers.clone(zone_change, true);
|
|
ICAL.Timezone.adjust_change(tmp_change, 0, 0, 0,
|
|
tmp_change.prevUtcOffset);
|
|
|
|
if (ICAL.Timezone._compare_change_fn(tt_change, tmp_change) < 0) {
|
|
var prev_zone_change = this.changes[change_num_to_use - 1];
|
|
|
|
var want_daylight = false; // TODO
|
|
|
|
if (zone_change.is_daylight != want_daylight &&
|
|
prev_zone_change.is_daylight == want_daylight) {
|
|
zone_change = prev_zone_change;
|
|
}
|
|
}
|
|
}
|
|
|
|
// TODO return is_daylight?
|
|
return zone_change.utcOffset;
|
|
},
|
|
|
|
_findNearbyChange: function icaltimezone_find_nearby_change(change) {
|
|
// find the closest match
|
|
var idx = ICAL.helpers.binsearchInsert(
|
|
this.changes,
|
|
change,
|
|
ICAL.Timezone._compare_change_fn
|
|
);
|
|
|
|
if (idx >= this.changes.length) {
|
|
return this.changes.length - 1;
|
|
}
|
|
|
|
return idx;
|
|
},
|
|
|
|
_ensureCoverage: function(aYear) {
|
|
if (ICAL.Timezone._minimumExpansionYear == -1) {
|
|
var today = ICAL.Time.now();
|
|
ICAL.Timezone._minimumExpansionYear = today.year;
|
|
}
|
|
|
|
var changesEndYear = aYear;
|
|
if (changesEndYear < ICAL.Timezone._minimumExpansionYear) {
|
|
changesEndYear = ICAL.Timezone._minimumExpansionYear;
|
|
}
|
|
|
|
changesEndYear += ICAL.Timezone.EXTRA_COVERAGE;
|
|
|
|
if (changesEndYear > ICAL.Timezone.MAX_YEAR) {
|
|
changesEndYear = ICAL.Timezone.MAX_YEAR;
|
|
}
|
|
|
|
if (!this.changes.length || this.expandedUntilYear < aYear) {
|
|
var subcomps = this.component.getAllSubcomponents();
|
|
var compLen = subcomps.length;
|
|
var compIdx = 0;
|
|
|
|
for (; compIdx < compLen; compIdx++) {
|
|
this._expandComponent(
|
|
subcomps[compIdx], changesEndYear, this.changes
|
|
);
|
|
}
|
|
|
|
this.changes.sort(ICAL.Timezone._compare_change_fn);
|
|
this.expandedUntilYear = changesEndYear;
|
|
}
|
|
},
|
|
|
|
_expandComponent: function(aComponent, aYear, changes) {
|
|
if (!aComponent.hasProperty("dtstart") ||
|
|
!aComponent.hasProperty("tzoffsetto") ||
|
|
!aComponent.hasProperty("tzoffsetfrom")) {
|
|
return null;
|
|
}
|
|
|
|
var dtstart = aComponent.getFirstProperty("dtstart").getFirstValue();
|
|
|
|
function convert_tzoffset(offset) {
|
|
return offset.factor * (offset.hours * 3600 + offset.minutes * 60);
|
|
}
|
|
|
|
function init_changes() {
|
|
var changebase = {};
|
|
changebase.is_daylight = (aComponent.name == "daylight");
|
|
changebase.utcOffset = convert_tzoffset(
|
|
aComponent.getFirstProperty("tzoffsetto").getFirstValue()
|
|
);
|
|
|
|
changebase.prevUtcOffset = convert_tzoffset(
|
|
aComponent.getFirstProperty("tzoffsetfrom").getFirstValue()
|
|
);
|
|
|
|
return changebase;
|
|
}
|
|
|
|
if (!aComponent.hasProperty("rrule") && !aComponent.hasProperty("rdate")) {
|
|
var change = init_changes();
|
|
change.year = dtstart.year;
|
|
change.month = dtstart.month;
|
|
change.day = dtstart.day;
|
|
change.hour = dtstart.hour;
|
|
change.minute = dtstart.minute;
|
|
change.second = dtstart.second;
|
|
|
|
ICAL.Timezone.adjust_change(change, 0, 0, 0,
|
|
-change.prevUtcOffset);
|
|
changes.push(change);
|
|
} else {
|
|
var props = aComponent.getAllProperties("rdate");
|
|
for (var rdatekey in props) {
|
|
var rdate = props[rdatekey];
|
|
var time = rdate.getFirstValue();
|
|
var change = init_changes();
|
|
|
|
change.year = time.year;
|
|
change.month = time.month;
|
|
change.day = time.day;
|
|
|
|
if (time.isDate) {
|
|
change.hour = dtstart.hour;
|
|
change.minute = dtstart.minute;
|
|
change.second = dtstart.second;
|
|
|
|
if (dtstart.zone != ICAL.Timezone.utcTimezone) {
|
|
ICAL.Timezone.adjust_change(change, 0, 0, 0,
|
|
-change.prevUtcOffset);
|
|
}
|
|
} else {
|
|
change.hour = time.hour;
|
|
change.minute = time.minute;
|
|
change.second = time.second;
|
|
|
|
if (time.zone != ICAL.Timezone.utcTimezone) {
|
|
ICAL.Timezone.adjust_change(change, 0, 0, 0,
|
|
-change.prevUtcOffset);
|
|
}
|
|
}
|
|
|
|
changes.push(change);
|
|
}
|
|
|
|
var rrule = aComponent.getFirstProperty("rrule");
|
|
|
|
if (rrule) {
|
|
rrule = rrule.getFirstValue();
|
|
var change = init_changes();
|
|
|
|
if (rrule.until && rrule.until.zone == ICAL.Timezone.utcTimezone) {
|
|
rrule.until.adjust(0, 0, 0, change.prevUtcOffset);
|
|
rrule.until.zone = ICAL.Timezone.localTimezone;
|
|
}
|
|
|
|
var iterator = rrule.iterator(dtstart);
|
|
|
|
var occ;
|
|
while ((occ = iterator.next())) {
|
|
var change = init_changes();
|
|
if (occ.year > aYear || !occ) {
|
|
break;
|
|
}
|
|
|
|
change.year = occ.year;
|
|
change.month = occ.month;
|
|
change.day = occ.day;
|
|
change.hour = occ.hour;
|
|
change.minute = occ.minute;
|
|
change.second = occ.second;
|
|
change.isDate = occ.isDate;
|
|
|
|
ICAL.Timezone.adjust_change(change, 0, 0, 0,
|
|
-change.prevUtcOffset);
|
|
changes.push(change);
|
|
}
|
|
}
|
|
}
|
|
|
|
return changes;
|
|
},
|
|
|
|
toString: function toString() {
|
|
return (this.tznames ? this.tznames : this.tzid);
|
|
}
|
|
|
|
};
|
|
|
|
ICAL.Timezone._compare_change_fn = function icaltimezone_compare_change_fn(a, b) {
|
|
if (a.year < b.year) return -1;
|
|
else if (a.year > b.year) return 1;
|
|
|
|
if (a.month < b.month) return -1;
|
|
else if (a.month > b.month) return 1;
|
|
|
|
if (a.day < b.day) return -1;
|
|
else if (a.day > b.day) return 1;
|
|
|
|
if (a.hour < b.hour) return -1;
|
|
else if (a.hour > b.hour) return 1;
|
|
|
|
if (a.minute < b.minute) return -1;
|
|
else if (a.minute > b.minute) return 1;
|
|
|
|
if (a.second < b.second) return -1;
|
|
else if (a.second > b.second) return 1;
|
|
|
|
return 0;
|
|
};
|
|
|
|
ICAL.Timezone.convert_time = function icaltimezone_convert_time(tt, from_zone, to_zone) {
|
|
if (tt.isDate ||
|
|
from_zone.tzid == to_zone.tzid ||
|
|
from_zone == ICAL.Timezone.localTimezone ||
|
|
to_zone == ICAL.Timezone.localTimezone) {
|
|
tt.zone = to_zone;
|
|
return tt;
|
|
}
|
|
|
|
var utcOffset = from_zone.utcOffset(tt);
|
|
tt.adjust(0, 0, 0, - utcOffset);
|
|
|
|
utcOffset = to_zone.utcOffset(tt);
|
|
tt.adjust(0, 0, 0, utcOffset);
|
|
|
|
return null;
|
|
};
|
|
|
|
ICAL.Timezone.fromData = function icaltimezone_fromData(aData) {
|
|
var tt = new ICAL.Timezone();
|
|
return tt.fromData(aData);
|
|
};
|
|
|
|
ICAL.Timezone.utcTimezone = ICAL.Timezone.fromData({
|
|
tzid: "UTC"
|
|
});
|
|
|
|
ICAL.Timezone.localTimezone = ICAL.Timezone.fromData({
|
|
tzid: "floating"
|
|
});
|
|
|
|
ICAL.Timezone.adjust_change = function icaltimezone_adjust_change(change, days, hours, minutes, seconds) {
|
|
return ICAL.Time.prototype.adjust.call(
|
|
change,
|
|
days,
|
|
hours,
|
|
minutes,
|
|
seconds,
|
|
change
|
|
);
|
|
};
|
|
|
|
ICAL.Timezone._minimumExpansionYear = -1;
|
|
ICAL.Timezone.MAX_YEAR = 2035; // TODO this is because of time_t, which we don't need. Still usefull?
|
|
ICAL.Timezone.EXTRA_COVERAGE = 5;
|
|
})();
|
|
// singleton class to contain timezones.
|
|
// Right now its all manual registry in the
|
|
// future we may use this class to download timezone
|
|
// information or handle loading pre-expanded timezones.
|
|
ICAL.TimezoneService = (function() {
|
|
var zones;
|
|
|
|
// Using var rather then return so we don't need to name the functions twice.
|
|
// TimezoneService#get will appear in profiler, etc...
|
|
var TimezoneService = {
|
|
reset: function() {
|
|
zones = Object.create(null);
|
|
var utc = ICAL.Timezone.utcTimezone;
|
|
|
|
zones.Z = utc;
|
|
zones.UTC = utc;
|
|
zones.GMT = utc;
|
|
},
|
|
|
|
/**
|
|
* Checks if timezone id has been registered.
|
|
*
|
|
* @param {String} tzid (e.g. America/Los_Angeles).
|
|
* @return {Boolean} false when not present.
|
|
*/
|
|
has: function(tzid) {
|
|
return !!zones[tzid];
|
|
},
|
|
|
|
/**
|
|
* Returns a timezone by its tzid if present.
|
|
*
|
|
* @param {String} tzid name of timezone (e.g. America/Los_Angeles).
|
|
* @return {ICAL.Timezone|Null} zone or null.
|
|
*/
|
|
get: function(tzid) {
|
|
return zones[tzid];
|
|
},
|
|
|
|
/**
|
|
* Registers a timezone object or component.
|
|
*
|
|
* @param {String} [name] optional uses timezone.tzid by default.
|
|
* @param {ICAL.Component|ICAL.Timezone} zone initialized zone or vtimezone.
|
|
*/
|
|
register: function(name, timezone) {
|
|
if (name instanceof ICAL.Component) {
|
|
if (name.name === 'vtimezone') {
|
|
timezone = new ICAL.Timezone(name);
|
|
name = timezone.tzid;
|
|
}
|
|
}
|
|
|
|
if (timezone instanceof ICAL.Timezone) {
|
|
zones[name] = timezone;
|
|
} else {
|
|
throw new TypeError('timezone must be ICAL.Timezone or ICAL.Component');
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Removes a timezone by its tzid from the list.
|
|
*
|
|
* @param {String} tzid (e.g. America/Los_Angeles).
|
|
*/
|
|
remove: function(tzid) {
|
|
return (delete zones[tzid]);
|
|
}
|
|
};
|
|
|
|
// initialize defaults
|
|
TimezoneService.reset();
|
|
|
|
return TimezoneService;
|
|
}());
|
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
* Portions Copyright (C) Philipp Kewisch, 2011-2012 */
|
|
|
|
|
|
|
|
(typeof(ICAL) === 'undefined')? ICAL = {} : '';
|
|
(function() {
|
|
|
|
/**
|
|
* Time representation (similar to JS Date object).
|
|
* Fully independent of system (OS) timezone / time.
|
|
* Unlike JS Date month start at 1 (Jan) not zero.
|
|
*
|
|
*
|
|
* var time = new ICAL.Time({
|
|
* year: 2012,
|
|
* month: 10,
|
|
* day: 11
|
|
* minute: 0,
|
|
* second: 0,
|
|
* isDate: false
|
|
* });
|
|
*
|
|
*
|
|
* @param {Object} data initialization time.
|
|
* @param {ICAL.Timezone} zone timezone this position occurs in.
|
|
*/
|
|
ICAL.Time = function icaltime(data, zone) {
|
|
this.wrappedJSObject = this;
|
|
var time = this._time = Object.create(null);
|
|
|
|
/* time defaults */
|
|
time.year = 0;
|
|
time.month = 1;
|
|
time.day = 1;
|
|
time.hour = 0;
|
|
time.minute = 0;
|
|
time.second = 0;
|
|
time.isDate = false;
|
|
|
|
this.fromData(data, zone);
|
|
};
|
|
|
|
ICAL.Time.prototype = {
|
|
|
|
icalclass: "icaltime",
|
|
|
|
// 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
|
|
* made and the next read operation must attempt to
|
|
* normalize the value (for example changing the day to 33).
|
|
*
|
|
* @type Boolean
|
|
* @private
|
|
*/
|
|
_pendingNormalization: false,
|
|
|
|
clone: function icaltime_clone() {
|
|
return new ICAL.Time(this._time, this.zone);
|
|
},
|
|
|
|
reset: function icaltime_reset() {
|
|
this.fromData(ICAL.Time.epochTime);
|
|
this.zone = ICAL.Timezone.utcTimezone;
|
|
},
|
|
|
|
resetTo: function icaltime_resetTo(year, month, day,
|
|
hour, minute, second, timezone) {
|
|
this.fromData({
|
|
year: year,
|
|
month: month,
|
|
day: day,
|
|
hour: hour,
|
|
minute: minute,
|
|
second: second,
|
|
zone: timezone
|
|
});
|
|
},
|
|
|
|
fromString: function icaltime_fromString(str) {
|
|
var data;
|
|
try {
|
|
data = ICAL.DecorationParser.parseValue(str, "date");
|
|
data.isDate = true;
|
|
} catch (e) {
|
|
data = ICAL.DecorationParser.parseValue(str, "date-time");
|
|
data.isDate = false;
|
|
}
|
|
return this.fromData(data);
|
|
},
|
|
|
|
fromJSDate: function icaltime_fromJSDate(aDate, useUTC) {
|
|
if (!aDate) {
|
|
this.reset();
|
|
} else {
|
|
if (useUTC) {
|
|
this.zone = ICAL.Timezone.utcTimezone;
|
|
this.year = aDate.getUTCFullYear();
|
|
this.month = aDate.getUTCMonth() + 1;
|
|
this.day = aDate.getUTCDate();
|
|
this.hour = aDate.getUTCHours();
|
|
this.minute = aDate.getUTCMinutes();
|
|
this.second = aDate.getUTCSeconds();
|
|
} else {
|
|
this.zone = ICAL.Timezone.localTimezone;
|
|
this.year = aDate.getFullYear();
|
|
this.month = aDate.getMonth() + 1;
|
|
this.day = aDate.getDate();
|
|
this.hour = aDate.getHours();
|
|
this.minute = aDate.getMinutes();
|
|
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;
|
|
}
|
|
|
|
if (aData && "timezone" in aData) {
|
|
var zone = ICAL.TimezoneService.get(
|
|
aData.timezone
|
|
);
|
|
|
|
this.zone = zone || ICAL.Timezone.localTimezone;
|
|
}
|
|
|
|
if (aData && "zone" in aData) {
|
|
this.zone = aData.zone;
|
|
}
|
|
|
|
if (!this.zone) {
|
|
this.zone = ICAL.Timezone.localTimezone;
|
|
}
|
|
|
|
return this;
|
|
},
|
|
|
|
dayOfWeek: function icaltime_dayOfWeek() {
|
|
// Using Zeller's algorithm
|
|
var q = this.day;
|
|
var m = this.month + (this.month < 3 ? 12 : 0);
|
|
var Y = this.year - (this.month < 3 ? 1 : 0);
|
|
|
|
var h = (q + Y + ICAL.helpers.trunc(((m + 1) * 26) / 10) + ICAL.helpers.trunc(Y / 4));
|
|
if (true /* gregorian */) {
|
|
h += ICAL.helpers.trunc(Y / 100) * 6 + ICAL.helpers.trunc(Y / 400);
|
|
} else {
|
|
h += 5;
|
|
}
|
|
|
|
// Normalize to 1 = sunday
|
|
h = ((h + 6) % 7) + 1;
|
|
return h;
|
|
},
|
|
|
|
dayOfYear: function icaltime_dayOfYear() {
|
|
var is_leap = (ICAL.Time.is_leap_year(this.year) ? 1 : 0);
|
|
var diypm = ICAL.Time._days_in_year_passed_month;
|
|
return diypm[is_leap][this.month - 1] + this.day;
|
|
},
|
|
|
|
startOfWeek: function startOfWeek() {
|
|
var result = this.clone();
|
|
result.day -= this.dayOfWeek() - 1;
|
|
return result;
|
|
},
|
|
|
|
endOfWeek: function endOfWeek() {
|
|
var result = this.clone();
|
|
result.day += 7 - this.dayOfWeek();
|
|
return result;
|
|
},
|
|
|
|
startOfMonth: function startOfMonth() {
|
|
var result = this.clone();
|
|
result.day = 1;
|
|
result.isDate = true;
|
|
result.hour = 0;
|
|
result.minute = 0;
|
|
result.second = 0;
|
|
return result;
|
|
},
|
|
|
|
endOfMonth: function endOfMonth() {
|
|
var result = this.clone();
|
|
result.day = ICAL.Time.daysInMonth(result.month, result.year);
|
|
result.isDate = true;
|
|
result.hour = 0;
|
|
result.minute = 0;
|
|
result.second = 0;
|
|
return result;
|
|
},
|
|
|
|
startOfYear: function startOfYear() {
|
|
var result = this.clone();
|
|
result.day = 1;
|
|
result.month = 1;
|
|
result.isDate = true;
|
|
result.hour = 0;
|
|
result.minute = 0;
|
|
result.second = 0;
|
|
return result;
|
|
},
|
|
|
|
endOfYear: function endOfYear() {
|
|
var result = this.clone();
|
|
result.day = 31;
|
|
result.month = 12;
|
|
result.isDate = true;
|
|
result.hour = 0;
|
|
result.minute = 0;
|
|
result.second = 0;
|
|
return result;
|
|
},
|
|
|
|
startDoyWeek: function startDoyWeek(aFirstDayOfWeek) {
|
|
var firstDow = aFirstDayOfWeek || ICAL.Time.SUNDAY;
|
|
var delta = this.dayOfWeek() - firstDow;
|
|
if (delta < 0) delta += 7;
|
|
return this.dayOfYear() - delta;
|
|
},
|
|
|
|
/**
|
|
* Finds the nthWeekDay relative to the current month (not day).
|
|
* The returned value is a day relative the month that this
|
|
* month belongs to so 1 would indicate the first of the month
|
|
* and 40 would indicate a day in the following month.
|
|
*
|
|
* @param {Numeric} aDayOfWeek day of the week see the day name constants.
|
|
* @param {Numeric} aPos nth occurrence of a given week day
|
|
* values of 1 and 0 both indicate the first
|
|
* weekday of that type. aPos may be either positive
|
|
* or negative.
|
|
*
|
|
* @return {Numeric} numeric value indicating a day relative
|
|
* to the current month of this time object.
|
|
*/
|
|
nthWeekDay: function icaltime_nthWeekDay(aDayOfWeek, aPos) {
|
|
var daysInMonth = ICAL.Time.daysInMonth(this.month, this.year);
|
|
var weekday;
|
|
var pos = aPos;
|
|
|
|
var start = 0;
|
|
|
|
var otherDay = this.clone();
|
|
|
|
if (pos >= 0) {
|
|
otherDay.day = 1;
|
|
|
|
// because 0 means no position has been given
|
|
// 1 and 0 indicate the same day.
|
|
if (pos != 0) {
|
|
// remove the extra numeric value
|
|
pos--;
|
|
}
|
|
|
|
// set current start offset to current day.
|
|
start = otherDay.day;
|
|
|
|
// find the current day of week
|
|
var startDow = otherDay.dayOfWeek();
|
|
|
|
// calculate the difference between current
|
|
// day of the week and desired day of the week
|
|
var offset = aDayOfWeek - startDow;
|
|
|
|
|
|
// if the offset goes into the past
|
|
// week we add 7 so its goes into the next
|
|
// week. We only want to go forward in time here.
|
|
if (offset < 0)
|
|
// this is really important otherwise we would
|
|
// end up with dates from in the past.
|
|
offset += 7;
|
|
|
|
// add offset to start so start is the same
|
|
// day of the week as the desired day of week.
|
|
start += offset;
|
|
|
|
// because we are going to add (and multiply)
|
|
// the numeric value of the day we subtract it
|
|
// from the start position so not to add it twice.
|
|
start -= aDayOfWeek;
|
|
|
|
// set week day
|
|
weekday = aDayOfWeek;
|
|
} else {
|
|
|
|
// then we set it to the last day in the current month
|
|
otherDay.day = daysInMonth;
|
|
|
|
// find the ends weekday
|
|
var endDow = otherDay.dayOfWeek();
|
|
|
|
pos++;
|
|
|
|
weekday = (endDow - aDayOfWeek);
|
|
|
|
if (weekday < 0) {
|
|
weekday += 7;
|
|
}
|
|
|
|
weekday = daysInMonth - weekday;
|
|
}
|
|
|
|
weekday += pos * 7;
|
|
|
|
return start + weekday;
|
|
},
|
|
|
|
/**
|
|
* Checks if current time is the nthWeekDay.
|
|
* Relative to the current month.
|
|
*
|
|
* Will always return false when rule resolves
|
|
* outside of current month.
|
|
*
|
|
* @param {Numeric} aDayOfWeek day of week.
|
|
* @param {Numeric} aPos position.
|
|
* @param {Numeric} aMax maximum valid day.
|
|
*/
|
|
isNthWeekDay: function(aDayOfWeek, aPos) {
|
|
var dow = this.dayOfWeek();
|
|
|
|
if (aPos === 0 && dow === aDayOfWeek) {
|
|
return true;
|
|
}
|
|
|
|
// get pos
|
|
var day = this.nthWeekDay(aDayOfWeek, aPos);
|
|
|
|
if (day === this.day) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
},
|
|
|
|
weekNumber: function weekNumber(aWeekStart) {
|
|
// This function courtesty of Julian Bucknall, published under the MIT license
|
|
// http://www.boyet.com/articles/publishedarticles/calculatingtheisoweeknumb.html
|
|
var doy = this.dayOfYear();
|
|
var dow = this.dayOfWeek();
|
|
var year = this.year;
|
|
var week1;
|
|
|
|
var dt = this.clone();
|
|
dt.isDate = true;
|
|
var first_dow = dt.dayOfWeek();
|
|
var isoyear = this.year;
|
|
|
|
if (dt.month == 12 && dt.day > 28) {
|
|
week1 = ICAL.Time.weekOneStarts(isoyear + 1, aWeekStart);
|
|
if (dt.compare(week1) < 0) {
|
|
week1 = ICAL.Time.weekOneStarts(isoyear, aWeekStart);
|
|
} else {
|
|
isoyear++;
|
|
}
|
|
} else {
|
|
week1 = ICAL.Time.weekOneStarts(isoyear, aWeekStart);
|
|
if (dt.compare(week1) < 0) {
|
|
week1 = ICAL.Time.weekOneStarts(--isoyear, aWeekStart);
|
|
}
|
|
}
|
|
|
|
var daysBetween = (dt.subtractDate(week1).toSeconds() / 86400);
|
|
return ICAL.helpers.trunc(daysBetween / 7) + 1;
|
|
},
|
|
|
|
addDuration: function icaltime_add(aDuration) {
|
|
var mult = (aDuration.isNegative ? -1 : 1);
|
|
|
|
// because of the duration optimizations it is much
|
|
// more efficient to grab all the values up front
|
|
// then set them directly (which will avoid a normalization call).
|
|
// So we don't actually normalize until we need it.
|
|
var second = this.second;
|
|
var minute = this.minute;
|
|
var hour = this.hour;
|
|
var day = this.day;
|
|
|
|
second += mult * aDuration.seconds;
|
|
minute += mult * aDuration.minutes;
|
|
hour += mult * aDuration.hours;
|
|
day += mult * aDuration.days;
|
|
day += mult * 7 * aDuration.weeks;
|
|
|
|
this.second = second;
|
|
this.minute = minute;
|
|
this.hour = hour;
|
|
this.day = day;
|
|
},
|
|
|
|
/**
|
|
* Subtract the date details (_excluding_ timezone).
|
|
* Useful for finding the relative difference between
|
|
* two time objects excluding their timezone differences.
|
|
*
|
|
* @return {ICAL.Duration} difference in duration.
|
|
*/
|
|
subtractDate: function icaltime_subtract(aDate) {
|
|
var unixTime = this.toUnixTime() + this.utcOffset();
|
|
var other = aDate.toUnixTime() + aDate.utcOffset();
|
|
return ICAL.Duration.fromSeconds(unixTime - other);
|
|
},
|
|
|
|
/**
|
|
* Subtract the date details, taking timezones into account.
|
|
*
|
|
* @param {ICAL.Time} The date to subtract.
|
|
* @return {ICAL.Duration} The difference in duration.
|
|
*/
|
|
subtractDateTz: function icaltime_subtract_abs(aDate) {
|
|
var unixTime = this.toUnixTime();
|
|
var other = aDate.toUnixTime();
|
|
return ICAL.Duration.fromSeconds(unixTime - other);
|
|
},
|
|
|
|
compare: function icaltime_compare(other) {
|
|
var a = this.toUnixTime();
|
|
var b = other.toUnixTime();
|
|
|
|
if (a > b) return 1;
|
|
if (b > a) return -1;
|
|
return 0;
|
|
},
|
|
|
|
compareDateOnlyTz: function icaltime_compareDateOnlyTz(other, tz) {
|
|
function cmp(attr) {
|
|
return ICAL.Time._cmp_attr(a, b, attr);
|
|
}
|
|
var a = this.convertToZone(tz);
|
|
var b = other.convertToZone(tz);
|
|
var rc = 0;
|
|
|
|
if ((rc = cmp("year")) != 0) return rc;
|
|
if ((rc = cmp("month")) != 0) return rc;
|
|
if ((rc = cmp("day")) != 0) return rc;
|
|
|
|
return rc;
|
|
},
|
|
|
|
convertToZone: function convertToZone(zone) {
|
|
var copy = this.clone();
|
|
var zone_equals = (this.zone.tzid == zone.tzid);
|
|
|
|
if (!this.isDate && !zone_equals) {
|
|
ICAL.Timezone.convert_time(copy, this.zone, zone);
|
|
}
|
|
|
|
copy.zone = zone;
|
|
return copy;
|
|
},
|
|
|
|
utcOffset: function utc_offset() {
|
|
if (this.zone == ICAL.Timezone.localTimezone ||
|
|
this.zone == ICAL.Timezone.utcTimezone) {
|
|
return 0;
|
|
} else {
|
|
return this.zone.utcOffset(this);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Returns an RFC 5455 compliant ical representation of this object.
|
|
*
|
|
* @return {String} ical date/date-time.
|
|
*/
|
|
toICALString: function() {
|
|
var string = this.toString();
|
|
|
|
if (string.length > 10) {
|
|
return ICAL.design.value['date-time'].toICAL(string);
|
|
} else {
|
|
return ICAL.design.value.date.toICAL(string);
|
|
}
|
|
},
|
|
|
|
toString: function toString() {
|
|
var result = this.year + '-' +
|
|
ICAL.helpers.pad2(this.month) + '-' +
|
|
ICAL.helpers.pad2(this.day);
|
|
|
|
if (!this.isDate) {
|
|
result += 'T' + ICAL.helpers.pad2(this.hour) + ':' +
|
|
ICAL.helpers.pad2(this.minute) + ':' +
|
|
ICAL.helpers.pad2(this.second);
|
|
|
|
if (this.zone === ICAL.Timezone.utcTimezone) {
|
|
result += 'Z';
|
|
}
|
|
}
|
|
|
|
return result;
|
|
},
|
|
|
|
toJSDate: function toJSDate() {
|
|
if (this.zone == ICAL.Timezone.localTimezone) {
|
|
if (this.isDate) {
|
|
return new Date(this.year, this.month - 1, this.day);
|
|
} else {
|
|
return new Date(this.year, this.month - 1, this.day,
|
|
this.hour, this.minute, this.second, 0);
|
|
}
|
|
} else {
|
|
return new Date(this.toUnixTime() * 1000);
|
|
}
|
|
},
|
|
|
|
_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.adjust(0, 0, 0, 0);
|
|
|
|
return this;
|
|
},
|
|
|
|
adjust: function icaltime_adjust(aExtraDays, aExtraHours,
|
|
aExtraMinutes, aExtraSeconds, aTime) {
|
|
|
|
var minutesOverflow, hoursOverflow,
|
|
daysOverflow = 0, yearsOverflow = 0;
|
|
|
|
var second, minute, hour, day;
|
|
var daysInMonth;
|
|
|
|
var time = aTime || this._time;
|
|
|
|
if (!time.isDate) {
|
|
second = time.second + aExtraSeconds;
|
|
time.second = second % 60;
|
|
minutesOverflow = ICAL.helpers.trunc(second / 60);
|
|
if (time.second < 0) {
|
|
time.second += 60;
|
|
minutesOverflow--;
|
|
}
|
|
|
|
minute = time.minute + aExtraMinutes + minutesOverflow;
|
|
time.minute = minute % 60;
|
|
hoursOverflow = ICAL.helpers.trunc(minute / 60);
|
|
if (time.minute < 0) {
|
|
time.minute += 60;
|
|
hoursOverflow--;
|
|
}
|
|
|
|
hour = time.hour + aExtraHours + hoursOverflow;
|
|
|
|
time.hour = hour % 24;
|
|
daysOverflow = ICAL.helpers.trunc(hour / 24);
|
|
if (time.hour < 0) {
|
|
time.hour += 24;
|
|
daysOverflow--;
|
|
}
|
|
}
|
|
|
|
|
|
// Adjust month and year first, because we need to know what month the day
|
|
// is in before adjusting it.
|
|
if (time.month > 12) {
|
|
yearsOverflow = ICAL.helpers.trunc((time.month - 1) / 12);
|
|
} else if (time.month < 1) {
|
|
yearsOverflow = ICAL.helpers.trunc(time.month / 12) - 1;
|
|
}
|
|
|
|
time.year += yearsOverflow;
|
|
time.month -= 12 * yearsOverflow;
|
|
|
|
// Now take care of the days (and adjust month if needed)
|
|
day = time.day + aExtraDays + daysOverflow;
|
|
|
|
if (day > 0) {
|
|
for (;;) {
|
|
var daysInMonth = ICAL.Time.daysInMonth(time.month, time.year);
|
|
if (day <= daysInMonth) {
|
|
break;
|
|
}
|
|
|
|
time.month++;
|
|
if (time.month > 12) {
|
|
time.year++;
|
|
time.month = 1;
|
|
}
|
|
|
|
day -= daysInMonth;
|
|
}
|
|
} else {
|
|
while (day <= 0) {
|
|
if (time.month == 1) {
|
|
time.year--;
|
|
time.month = 12;
|
|
} else {
|
|
time.month--;
|
|
}
|
|
|
|
day += ICAL.Time.daysInMonth(time.month, time.year);
|
|
}
|
|
}
|
|
|
|
time.day = day;
|
|
return this;
|
|
},
|
|
|
|
fromUnixTime: function fromUnixTime(seconds) {
|
|
this.zone = ICAL.Timezone.utcTimezone;
|
|
var epoch = ICAL.Time.epochTime.clone();
|
|
epoch.adjust(0, 0, 0, seconds);
|
|
|
|
this.year = epoch.year;
|
|
this.month = epoch.month;
|
|
this.day = epoch.day;
|
|
this.hour = epoch.hour;
|
|
this.minute = epoch.minute;
|
|
this.second = epoch.second;
|
|
},
|
|
|
|
toUnixTime: function toUnixTime() {
|
|
var offset = this.utcOffset();
|
|
|
|
// we use the offset trick to ensure
|
|
// that we are getting the actual UTC time
|
|
var ms = Date.UTC(
|
|
this.year,
|
|
this.month - 1,
|
|
this.day,
|
|
this.hour,
|
|
this.minute,
|
|
this.second - offset
|
|
);
|
|
|
|
// seconds
|
|
return ms / 1000;
|
|
},
|
|
|
|
/**
|
|
* Converts time to into Object
|
|
* which can be serialized then re-created
|
|
* using the constructor.
|
|
*
|
|
* Example:
|
|
*
|
|
* // toJSON will automatically be called
|
|
* var json = JSON.stringify(mytime);
|
|
*
|
|
* var deserialized = JSON.parse(json);
|
|
*
|
|
* var time = new ICAL.Time(deserialized);
|
|
*
|
|
*/
|
|
toJSON: function() {
|
|
var copy = [
|
|
'year',
|
|
'month',
|
|
'day',
|
|
'hour',
|
|
'minute',
|
|
'second',
|
|
'isDate'
|
|
];
|
|
|
|
var result = Object.create(null);
|
|
|
|
var i = 0;
|
|
var len = copy.length;
|
|
var prop;
|
|
|
|
for (; i < len; i++) {
|
|
prop = copy[i];
|
|
result[prop] = this[prop];
|
|
}
|
|
|
|
if (this.zone) {
|
|
result.timezone = this.zone.tzid;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
};
|
|
|
|
(function setupNormalizeAttributes() {
|
|
// This needs to run before any instances are created!
|
|
function defineAttr(attr) {
|
|
Object.defineProperty(ICAL.Time.prototype, attr, {
|
|
get: function getTimeAttr() {
|
|
if (this._pendingNormalization) {
|
|
this._normalize();
|
|
this._pendingNormalization = false;
|
|
}
|
|
|
|
return this._time[attr];
|
|
},
|
|
set: function setTimeAttr(val) {
|
|
this._pendingNormalization = true;
|
|
this._time[attr] = val;
|
|
|
|
return val;
|
|
}
|
|
});
|
|
|
|
}
|
|
|
|
if ("defineProperty" in Object) {
|
|
defineAttr("year");
|
|
defineAttr("month");
|
|
defineAttr("day");
|
|
defineAttr("hour");
|
|
defineAttr("minute");
|
|
defineAttr("second");
|
|
defineAttr("isDate");
|
|
}
|
|
})();
|
|
|
|
ICAL.Time.daysInMonth = function icaltime_daysInMonth(month, year) {
|
|
var _daysInMonth = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
|
var days = 30;
|
|
|
|
if (month < 1 || month > 12) return days;
|
|
|
|
days = _daysInMonth[month];
|
|
|
|
if (month == 2) {
|
|
days += ICAL.Time.is_leap_year(year);
|
|
}
|
|
|
|
return days;
|
|
};
|
|
|
|
ICAL.Time.is_leap_year = function icaltime_is_leap_year(year) {
|
|
if (year <= 1752) {
|
|
return ((year % 4) == 0);
|
|
} else {
|
|
return (((year % 4 == 0) && (year % 100 != 0)) || (year % 400 == 0));
|
|
}
|
|
};
|
|
|
|
ICAL.Time.fromDayOfYear = function icaltime_fromDayOfYear(aDayOfYear, aYear) {
|
|
var year = aYear;
|
|
var doy = aDayOfYear;
|
|
var tt = new ICAL.Time();
|
|
tt.auto_normalize = false;
|
|
var is_leap = (ICAL.Time.is_leap_year(year) ? 1 : 0);
|
|
|
|
if (doy < 1) {
|
|
year--;
|
|
is_leap = (ICAL.Time.is_leap_year(year) ? 1 : 0);
|
|
doy += ICAL.Time._days_in_year_passed_month[is_leap][12];
|
|
} else if (doy > ICAL.Time._days_in_year_passed_month[is_leap][12]) {
|
|
is_leap = (ICAL.Time.is_leap_year(year) ? 1 : 0);
|
|
doy -= ICAL.Time._days_in_year_passed_month[is_leap][12];
|
|
year++;
|
|
}
|
|
|
|
tt.year = year;
|
|
tt.isDate = true;
|
|
|
|
for (var month = 11; month >= 0; month--) {
|
|
if (doy > ICAL.Time._days_in_year_passed_month[is_leap][month]) {
|
|
tt.month = month + 1;
|
|
tt.day = doy - ICAL.Time._days_in_year_passed_month[is_leap][month];
|
|
break;
|
|
}
|
|
}
|
|
|
|
tt.auto_normalize = true;
|
|
return tt;
|
|
};
|
|
|
|
ICAL.Time.fromStringv2 = function fromString(str) {
|
|
return new ICAL.Time({
|
|
year: parseInt(str.substr(0, 4), 10),
|
|
month: parseInt(str.substr(5, 2), 10),
|
|
day: parseInt(str.substr(8, 2), 10),
|
|
isDate: true
|
|
});
|
|
};
|
|
|
|
ICAL.Time.fromDateString = function(aValue, aProp) {
|
|
// Dates should have no timezone.
|
|
// Google likes to sometimes specify Z on dates
|
|
// we specifically ignore that to avoid issues.
|
|
|
|
// YYYY-MM-DD
|
|
// 2012-10-10
|
|
return new ICAL.Time({
|
|
year: ICAL.helpers.strictParseInt(aValue.substr(0, 4)),
|
|
month: ICAL.helpers.strictParseInt(aValue.substr(5, 2)),
|
|
day: ICAL.helpers.strictParseInt(aValue.substr(8, 2)),
|
|
isDate: true
|
|
});
|
|
};
|
|
|
|
ICAL.Time.fromDateTimeString = function(aValue, prop) {
|
|
if (aValue.length < 19) {
|
|
throw new Error(
|
|
'invalid date-time value: "' + aValue + '"'
|
|
);
|
|
}
|
|
|
|
var zone;
|
|
|
|
if (aValue[19] === 'Z') {
|
|
zone = 'Z';
|
|
} else if (prop) {
|
|
zone = prop.getParameter('tzid');
|
|
}
|
|
|
|
// 2012-10-10T10:10:10(Z)?
|
|
var time = new ICAL.Time({
|
|
year: ICAL.helpers.strictParseInt(aValue.substr(0, 4)),
|
|
month: ICAL.helpers.strictParseInt(aValue.substr(5, 2)),
|
|
day: ICAL.helpers.strictParseInt(aValue.substr(8, 2)),
|
|
hour: ICAL.helpers.strictParseInt(aValue.substr(11, 2)),
|
|
minute: ICAL.helpers.strictParseInt(aValue.substr(14, 2)),
|
|
second: ICAL.helpers.strictParseInt(aValue.substr(17, 2)),
|
|
timezone: zone
|
|
});
|
|
|
|
return time;
|
|
};
|
|
|
|
ICAL.Time.fromString = function fromString(aValue) {
|
|
if (aValue.length > 10) {
|
|
return ICAL.Time.fromDateTimeString(aValue);
|
|
} else {
|
|
return ICAL.Time.fromDateString(aValue);
|
|
}
|
|
};
|
|
|
|
ICAL.Time.fromJSDate = function fromJSDate(aDate, useUTC) {
|
|
var tt = new ICAL.Time();
|
|
return tt.fromJSDate(aDate, useUTC);
|
|
};
|
|
|
|
ICAL.Time.fromData = function fromData(aData) {
|
|
var t = new ICAL.Time();
|
|
return t.fromData(aData);
|
|
};
|
|
|
|
ICAL.Time.now = function icaltime_now() {
|
|
return ICAL.Time.fromJSDate(new Date(), false);
|
|
};
|
|
|
|
ICAL.Time.weekOneStarts = function weekOneStarts(aYear, aWeekStart) {
|
|
var t = ICAL.Time.fromData({
|
|
year: aYear,
|
|
month: 1,
|
|
day: 4,
|
|
isDate: true
|
|
});
|
|
|
|
var fourth_dow = t.dayOfWeek();
|
|
t.day += (1 - fourth_dow) + ((aWeekStart || ICAL.Time.SUNDAY) - 1);
|
|
return t;
|
|
};
|
|
|
|
ICAL.Time.epochTime = ICAL.Time.fromData({
|
|
year: 1970,
|
|
month: 1,
|
|
day: 1,
|
|
hour: 0,
|
|
minute: 0,
|
|
second: 0,
|
|
isDate: false,
|
|
timezone: "Z"
|
|
});
|
|
|
|
ICAL.Time._cmp_attr = function _cmp_attr(a, b, attr) {
|
|
if (a[attr] > b[attr]) return 1;
|
|
if (a[attr] < b[attr]) return -1;
|
|
return 0;
|
|
};
|
|
|
|
ICAL.Time._days_in_year_passed_month = [
|
|
[0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365],
|
|
[0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366]
|
|
];
|
|
|
|
|
|
ICAL.Time.SUNDAY = 1;
|
|
ICAL.Time.MONDAY = 2;
|
|
ICAL.Time.TUESDAY = 3;
|
|
ICAL.Time.WEDNESDAY = 4;
|
|
ICAL.Time.THURSDAY = 5;
|
|
ICAL.Time.FRIDAY = 6;
|
|
ICAL.Time.SATURDAY = 7;
|
|
|
|
ICAL.Time.DEFAULT_WEEK_START = ICAL.Time.MONDAY;
|
|
})();
|
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
* Portions Copyright (C) Philipp Kewisch, 2011-2012 */
|
|
|
|
|
|
|
|
(typeof(ICAL) === 'undefined')? ICAL = {} : '';
|
|
(function() {
|
|
|
|
var DOW_MAP = {
|
|
SU: ICAL.Time.SUNDAY,
|
|
MO: ICAL.Time.MONDAY,
|
|
TU: ICAL.Time.TUESDAY,
|
|
WE: ICAL.Time.WEDNESDAY,
|
|
TH: ICAL.Time.THURSDAY,
|
|
FR: ICAL.Time.FRIDAY,
|
|
SA: ICAL.Time.SATURDAY
|
|
};
|
|
|
|
var REVERSE_DOW_MAP = {};
|
|
for (var key in DOW_MAP) {
|
|
REVERSE_DOW_MAP[DOW_MAP[key]] = key;
|
|
}
|
|
|
|
var COPY_PARTS = ["BYSECOND", "BYMINUTE", "BYHOUR", "BYDAY",
|
|
"BYMONTHDAY", "BYYEARDAY", "BYWEEKNO",
|
|
"BYMONTH", "BYSETPOS"];
|
|
|
|
ICAL.Recur = function icalrecur(data) {
|
|
this.wrappedJSObject = this;
|
|
this.parts = {};
|
|
|
|
if (typeof(data) === 'object') {
|
|
for (var key in data) {
|
|
this[key] = data[key];
|
|
}
|
|
|
|
if (this.until && !(this.until instanceof ICAL.Time)) {
|
|
this.until = new ICAL.Time(this.until);
|
|
}
|
|
}
|
|
|
|
if (!this.parts) {
|
|
this.parts = {};
|
|
}
|
|
};
|
|
|
|
ICAL.Recur.prototype = {
|
|
|
|
parts: null,
|
|
|
|
interval: 1,
|
|
wkst: ICAL.Time.MONDAY,
|
|
until: null,
|
|
count: null,
|
|
freq: null,
|
|
icalclass: "icalrecur",
|
|
icaltype: "recur",
|
|
|
|
iterator: function(aStart) {
|
|
return new ICAL.RecurIterator({
|
|
rule: this,
|
|
dtstart: aStart
|
|
});
|
|
},
|
|
|
|
clone: function clone() {
|
|
return new ICAL.Recur(this.toJSON());
|
|
},
|
|
|
|
isFinite: function isfinite() {
|
|
return !!(this.count || this.until);
|
|
},
|
|
|
|
isByCount: function isbycount() {
|
|
return !!(this.count && !this.until);
|
|
},
|
|
|
|
addComponent: function addPart(aType, aValue) {
|
|
if (!(aType in this.parts)) {
|
|
this.parts[aType] = [aValue];
|
|
} else {
|
|
this.parts[aType].push(aValue);
|
|
}
|
|
},
|
|
|
|
setComponent: function setComponent(aType, aValues) {
|
|
this.parts[aType] = aValues;
|
|
},
|
|
|
|
getComponent: function getComponent(aType, aCount) {
|
|
var ucName = aType.toUpperCase();
|
|
var components = (ucName in this.parts ? this.parts[ucName] : []);
|
|
|
|
if (aCount) aCount.value = components.length;
|
|
return components;
|
|
},
|
|
|
|
getNextOccurrence: function getNextOccurrence(aStartTime, aRecurrenceId) {
|
|
var iter = this.iterator(aStartTime);
|
|
var next, cdt;
|
|
|
|
do {
|
|
next = iter.next();
|
|
} while (next && next.compare(aRecurrenceId) <= 0);
|
|
|
|
if (next && aRecurrenceId.zone) {
|
|
next.zone = aRecurrenceId.zone;
|
|
}
|
|
|
|
return next;
|
|
},
|
|
|
|
toJSON: function() {
|
|
//XXX: extract this list up to proto?
|
|
var propsToCopy = [
|
|
"freq",
|
|
"count",
|
|
"until",
|
|
"wkst",
|
|
"interval",
|
|
"parts"
|
|
];
|
|
|
|
var result = Object.create(null);
|
|
|
|
var i = 0;
|
|
var len = propsToCopy.length;
|
|
var prop;
|
|
|
|
for (; i < len; i++) {
|
|
var prop = propsToCopy[i];
|
|
result[prop] = this[prop];
|
|
}
|
|
|
|
if (result.until instanceof ICAL.Time) {
|
|
result.until = result.until.toJSON();
|
|
}
|
|
|
|
return result;
|
|
},
|
|
|
|
toString: function icalrecur_toString() {
|
|
// TODO retain order
|
|
var str = "FREQ=" + this.freq;
|
|
if (this.count) {
|
|
str += ";COUNT=" + this.count;
|
|
}
|
|
if (this.interval > 1) {
|
|
str += ";INTERVAL=" + this.interval;
|
|
}
|
|
for (var k in this.parts) {
|
|
str += ";" + k + "=" + this.parts[k];
|
|
}
|
|
if (this.until ){
|
|
str += ';UNTIL=' + this.until.toString();
|
|
}
|
|
if ('wkst' in this && this.wkst !== ICAL.Time.DEFAULT_WEEK_START) {
|
|
str += ';WKST=' + ICAL.Recur.numericDayToIcalDay(this.wkst);
|
|
}
|
|
return str;
|
|
}
|
|
};
|
|
|
|
function parseNumericValue(type, min, max, value) {
|
|
var result = value;
|
|
|
|
if (value[0] === '+') {
|
|
result = value.substr(1);
|
|
}
|
|
|
|
result = ICAL.helpers.strictParseInt(result);
|
|
|
|
if (min !== undefined && value < min) {
|
|
throw new Error(
|
|
type + ': invalid value "' + value + '" must be > ' + min
|
|
);
|
|
}
|
|
|
|
if (max !== undefined && value > max) {
|
|
throw new Error(
|
|
type + ': invalid value "' + value + '" must be < ' + min
|
|
);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Convert an ical representation of a day (SU, MO, etc..)
|
|
* into a numeric value of that day.
|
|
*
|
|
* @param {String} day ical day.
|
|
* @return {Numeric} numeric value of given day.
|
|
*/
|
|
ICAL.Recur.icalDayToNumericDay = function toNumericDay(string) {
|
|
//XXX: this is here so we can deal
|
|
// with possibly invalid string values.
|
|
|
|
return DOW_MAP[string];
|
|
};
|
|
|
|
/**
|
|
* Convert a numeric day value into its ical representation (SU, MO, etc..)
|
|
*
|
|
* @param {Numeric} numeric value of given day.
|
|
* @return {String} day ical day.
|
|
*/
|
|
ICAL.Recur.numericDayToIcalDay = function toIcalDay(num) {
|
|
//XXX: this is here so we can deal with possibly invalid number values.
|
|
// Also, this allows consistent mapping between day numbers and day
|
|
// names for external users.
|
|
return REVERSE_DOW_MAP[num];
|
|
};
|
|
|
|
var VALID_DAY_NAMES = /^(SU|MO|TU|WE|TH|FR|SA)$/;
|
|
var VALID_BYDAY_PART = /^([+-])?(5[0-3]|[1-4][0-9]|[1-9])?(SU|MO|TU|WE|TH|FR|SA)$/;
|
|
var ALLOWED_FREQ = ['SECONDLY', 'MINUTELY', 'HOURLY',
|
|
'DAILY', 'WEEKLY', 'MONTHLY', 'YEARLY'];
|
|
|
|
var optionDesign = {
|
|
FREQ: function(value, dict) {
|
|
// yes this is actually equal or faster then regex.
|
|
// upside here is we can enumerate the valid values.
|
|
if (ALLOWED_FREQ.indexOf(value) !== -1) {
|
|
dict.freq = value;
|
|
} else {
|
|
throw new Error(
|
|
'invalid frequency "' + value + '" expected: "' +
|
|
ALLOWED_FREQ.join(', ') + '"'
|
|
);
|
|
}
|
|
},
|
|
|
|
COUNT: function(value, dict) {
|
|
dict.count = ICAL.helpers.strictParseInt(value);
|
|
},
|
|
|
|
INTERVAL: function(value, dict) {
|
|
dict.interval = ICAL.helpers.strictParseInt(value);
|
|
if (dict.interval < 1) {
|
|
// 0 or negative values are not allowed, some engines seem to generate
|
|
// it though. Assume 1 instead.
|
|
dict.interval = 1;
|
|
}
|
|
},
|
|
|
|
UNTIL: function(value, dict) {
|
|
dict.until = ICAL.Time.fromString(value);
|
|
},
|
|
|
|
WKST: function(value, dict) {
|
|
if (VALID_DAY_NAMES.test(value)) {
|
|
dict.wkst = ICAL.Recur.icalDayToNumericDay(value);
|
|
} else {
|
|
throw new Error('invalid WKST value "' + value + '"');
|
|
}
|
|
}
|
|
};
|
|
|
|
var partDesign = {
|
|
BYSECOND: parseNumericValue.bind(this, 'BYSECOND', 0, 60),
|
|
BYMINUTE: parseNumericValue.bind(this, 'BYMINUTE', 0, 59),
|
|
BYHOUR: parseNumericValue.bind(this, 'BYHOUR', 0, 23),
|
|
BYDAY: function(value) {
|
|
if (VALID_BYDAY_PART.test(value)) {
|
|
return value;
|
|
} else {
|
|
throw new Error('invalid BYDAY value "' + value + '"');
|
|
}
|
|
},
|
|
BYMONTHDAY: parseNumericValue.bind(this, 'BYMONTHDAY', -31, 31),
|
|
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 = 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('=');
|
|
var name = parts[0];
|
|
var value = parts[1];
|
|
|
|
if (name in partDesign) {
|
|
var partArr = value.split(',');
|
|
var partArrIdx = 0;
|
|
var partArrLen = partArr.length;
|
|
|
|
for (; partArrIdx < partArrLen; partArrIdx++) {
|
|
partArr[partArrIdx] = partDesign[name](partArr[partArrIdx]);
|
|
}
|
|
dictParts[name] = partArr;
|
|
} else if (name in optionDesign) {
|
|
optionDesign[name](value, dict);
|
|
}
|
|
}
|
|
|
|
return new ICAL.Recur(dict);
|
|
};
|
|
|
|
})();
|
|
ICAL.RecurIterator = (function() {
|
|
|
|
/**
|
|
* Options:
|
|
* - rule: (ICAL.Recur) instance
|
|
* - dtstart: (ICAL.Time) start date of recurrence rule
|
|
* - initialized: (Boolean) when true will assume options
|
|
* are from previously constructed
|
|
* iterator and will not re-initialize
|
|
* iterator but resume its state from given data.
|
|
*
|
|
* - by_data: (for iterator de-serialization)
|
|
* - days: "
|
|
* - last: "
|
|
* - by_indices: "
|
|
*/
|
|
function icalrecur_iterator(options) {
|
|
this.fromData(options);
|
|
}
|
|
|
|
icalrecur_iterator.prototype = {
|
|
|
|
/**
|
|
* True when iteration is finished.
|
|
*/
|
|
completed: false,
|
|
|
|
rule: null,
|
|
dtstart: null,
|
|
last: null,
|
|
occurrence_number: 0,
|
|
by_indices: null,
|
|
initialized: false,
|
|
by_data: null,
|
|
|
|
days: null,
|
|
days_index: 0,
|
|
|
|
fromData: function(options) {
|
|
this.rule = ICAL.helpers.formatClassType(options.rule, ICAL.Recur);
|
|
|
|
if (!this.rule) {
|
|
throw new Error('iterator requires a (ICAL.Recur) rule');
|
|
}
|
|
|
|
this.dtstart = ICAL.helpers.formatClassType(options.dtstart, ICAL.Time);
|
|
|
|
if (!this.dtstart) {
|
|
throw new Error('iterator requires a (ICAL.Time) dtstart');
|
|
}
|
|
|
|
if (options.by_data) {
|
|
this.by_data = options.by_data;
|
|
} else {
|
|
this.by_data = ICAL.helpers.clone(this.rule.parts, true);
|
|
}
|
|
|
|
if (options.occurrence_number)
|
|
this.occurrence_number = options.occurrence_number;
|
|
|
|
this.days = options.days || [];
|
|
this.last = ICAL.helpers.formatClassType(options.last, ICAL.Time);
|
|
|
|
this.by_indices = options.by_indices;
|
|
|
|
if (!this.by_indices) {
|
|
this.by_indices = {
|
|
"BYSECOND": 0,
|
|
"BYMINUTE": 0,
|
|
"BYHOUR": 0,
|
|
"BYDAY": 0,
|
|
"BYMONTH": 0,
|
|
"BYWEEKNO": 0,
|
|
"BYMONTHDAY": 0
|
|
};
|
|
}
|
|
|
|
this.initialized = options.initialized || false;
|
|
|
|
if (!this.initialized) {
|
|
this.init();
|
|
}
|
|
},
|
|
|
|
init: function icalrecur_iterator_init() {
|
|
this.initialized = true;
|
|
this.last = this.dtstart.clone();
|
|
var parts = this.by_data;
|
|
|
|
if ("BYDAY" in parts) {
|
|
// libical does this earlier when the rule is loaded, but we postpone to
|
|
// now so we can preserve the original order.
|
|
this.sort_byday_rules(parts.BYDAY, this.rule.wkst);
|
|
}
|
|
|
|
// If the BYYEARDAY appares, no other date rule part may appear
|
|
if ("BYYEARDAY" in parts) {
|
|
if ("BYMONTH" in parts || "BYWEEKNO" in parts ||
|
|
"BYMONTHDAY" in parts || "BYDAY" in parts) {
|
|
throw new Error("Invalid BYYEARDAY rule");
|
|
}
|
|
}
|
|
|
|
// BYWEEKNO and BYMONTHDAY rule parts may not both appear
|
|
if ("BYWEEKNO" in parts && "BYMONTHDAY" in parts) {
|
|
throw new Error("BYWEEKNO does not fit to BYMONTHDAY");
|
|
}
|
|
|
|
// For MONTHLY recurrences (FREQ=MONTHLY) neither BYYEARDAY nor
|
|
// BYWEEKNO may appear.
|
|
if (this.rule.freq == "MONTHLY" &&
|
|
("BYYEARDAY" in parts || "BYWEEKNO" in parts)) {
|
|
throw new Error("For MONTHLY recurrences neither BYYEARDAY nor BYWEEKNO may appear");
|
|
}
|
|
|
|
// For WEEKLY recurrences (FREQ=WEEKLY) neither BYMONTHDAY nor
|
|
// BYYEARDAY may appear.
|
|
if (this.rule.freq == "WEEKLY" &&
|
|
("BYYEARDAY" in parts || "BYMONTHDAY" in parts)) {
|
|
throw new Error("For WEEKLY recurrences neither BYMONTHDAY nor BYYEARDAY may appear");
|
|
}
|
|
|
|
// BYYEARDAY may only appear in YEARLY rules
|
|
if (this.rule.freq != "YEARLY" && "BYYEARDAY" in parts) {
|
|
throw new Error("BYYEARDAY may only appear in YEARLY rules");
|
|
}
|
|
|
|
this.last.second = this.setup_defaults("BYSECOND", "SECONDLY", this.dtstart.second);
|
|
this.last.minute = this.setup_defaults("BYMINUTE", "MINUTELY", this.dtstart.minute);
|
|
this.last.hour = this.setup_defaults("BYHOUR", "HOURLY", this.dtstart.hour);
|
|
this.last.day = this.setup_defaults("BYMONTHDAY", "DAILY", this.dtstart.day);
|
|
this.last.month = this.setup_defaults("BYMONTH", "MONTHLY", this.dtstart.month);
|
|
|
|
if (this.rule.freq == "WEEKLY") {
|
|
if ("BYDAY" in parts) {
|
|
var parts = this.ruleDayOfWeek(parts.BYDAY[0]);
|
|
var pos = parts[0];
|
|
var rule_dow = parts[1];
|
|
var dow = rule_dow - this.last.dayOfWeek();
|
|
if ((this.last.dayOfWeek() < rule_dow && dow >= 0) || dow < 0) {
|
|
// Initial time is after first day of BYDAY data
|
|
this.last.day += dow;
|
|
}
|
|
} else {
|
|
var dayName = ICAL.Recur.numericDayToIcalDay(this.dtstart.dayOfWeek());
|
|
parts.BYDAY = [dayName];
|
|
}
|
|
}
|
|
|
|
if (this.rule.freq == "YEARLY") {
|
|
for (;;) {
|
|
this.expand_year_days(this.last.year);
|
|
if (this.days.length > 0) {
|
|
break;
|
|
}
|
|
this.increment_year(this.rule.interval);
|
|
}
|
|
|
|
var next = ICAL.Time.fromDayOfYear(this.days[0], this.last.year);
|
|
|
|
this.last.day = next.day;
|
|
this.last.month = next.month;
|
|
}
|
|
|
|
if (this.rule.freq == "MONTHLY" && this.has_by_data("BYDAY")) {
|
|
|
|
var coded_day = this.by_data.BYDAY[this.by_indices.BYDAY];
|
|
var parts = this.ruleDayOfWeek(coded_day);
|
|
var pos = parts[0];
|
|
var dow = parts[1];
|
|
|
|
var daysInMonth = ICAL.Time.daysInMonth(this.last.month, this.last.year);
|
|
var poscount = 0;
|
|
|
|
if (pos >= 0) {
|
|
for (this.last.day = 1; this.last.day <= daysInMonth; this.last.day++) {
|
|
if (this.last.dayOfWeek() == dow) {
|
|
if (++poscount == pos || pos == 0) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
pos = -pos;
|
|
for (this.last.day = daysInMonth; this.last.day != 0; this.last.day--) {
|
|
if (this.last.dayOfWeek() == dow) {
|
|
if (++poscount == pos) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//XXX: This feels like a hack, but we need to initialize
|
|
// the BYMONTHDAY case correctly and byDayAndMonthDay handles
|
|
// this case. It accepts a special flag which will avoid incrementing
|
|
// the initial value without the flag days that match the start time
|
|
// would be missed.
|
|
if (this.has_by_data('BYMONTHDAY')) {
|
|
this._byDayAndMonthDay(true);
|
|
}
|
|
|
|
if (this.last.day > daysInMonth || this.last.day == 0) {
|
|
throw new Error("Malformed values in BYDAY part");
|
|
}
|
|
|
|
} else if (this.has_by_data("BYMONTHDAY")) {
|
|
if (this.last.day < 0) {
|
|
var daysInMonth = ICAL.Time.daysInMonth(this.last.month, this.last.year);
|
|
this.last.day = daysInMonth + this.last.day + 1;
|
|
}
|
|
}
|
|
|
|
},
|
|
|
|
next: function icalrecur_iterator_next() {
|
|
var before = (this.last ? this.last.clone() : null);
|
|
|
|
if ((this.rule.count && this.occurrence_number >= this.rule.count) ||
|
|
(this.rule.until && this.last.compare(this.rule.until) > 0)) {
|
|
|
|
//XXX: right now this is just a flag and has no impact
|
|
// we can simplify the above case to check for completed later.
|
|
this.completed = true;
|
|
|
|
return null;
|
|
}
|
|
|
|
if (this.occurrence_number == 0 && this.last.compare(this.dtstart) >= 0) {
|
|
// First of all, give the instance that was initialized
|
|
this.occurrence_number++;
|
|
return this.last;
|
|
}
|
|
|
|
do {
|
|
var valid = 1;
|
|
|
|
switch (this.rule.freq) {
|
|
case "SECONDLY":
|
|
this.next_second();
|
|
break;
|
|
case "MINUTELY":
|
|
this.next_minute();
|
|
break;
|
|
case "HOURLY":
|
|
this.next_hour();
|
|
break;
|
|
case "DAILY":
|
|
this.next_day();
|
|
break;
|
|
case "WEEKLY":
|
|
this.next_week();
|
|
break;
|
|
case "MONTHLY":
|
|
valid = this.next_month();
|
|
break;
|
|
case "YEARLY":
|
|
this.next_year();
|
|
break;
|
|
|
|
default:
|
|
return null;
|
|
}
|
|
} while (!this.check_contracting_rules() ||
|
|
this.last.compare(this.dtstart) < 0 ||
|
|
!valid);
|
|
|
|
// TODO is this valid?
|
|
if (this.last.compare(before) == 0) {
|
|
throw new Error("Same occurrence found twice, protecting " +
|
|
"you from death by recursion");
|
|
}
|
|
|
|
if (this.rule.until && this.last.compare(this.rule.until) > 0) {
|
|
this.completed = true;
|
|
return null;
|
|
} else {
|
|
this.occurrence_number++;
|
|
return this.last;
|
|
}
|
|
},
|
|
|
|
next_second: function next_second() {
|
|
return this.next_generic("BYSECOND", "SECONDLY", "second", "minute");
|
|
},
|
|
|
|
increment_second: function increment_second(inc) {
|
|
return this.increment_generic(inc, "second", 60, "minute");
|
|
},
|
|
|
|
next_minute: function next_minute() {
|
|
return this.next_generic("BYMINUTE", "MINUTELY",
|
|
"minute", "hour", "next_second");
|
|
},
|
|
|
|
increment_minute: function increment_minute(inc) {
|
|
return this.increment_generic(inc, "minute", 60, "hour");
|
|
},
|
|
|
|
next_hour: function next_hour() {
|
|
return this.next_generic("BYHOUR", "HOURLY", "hour",
|
|
"monthday", "next_minute");
|
|
},
|
|
|
|
increment_hour: function increment_hour(inc) {
|
|
this.increment_generic(inc, "hour", 24, "monthday");
|
|
},
|
|
|
|
next_day: function next_day() {
|
|
var has_by_day = ("BYDAY" in this.by_data);
|
|
var this_freq = (this.rule.freq == "DAILY");
|
|
|
|
if (this.next_hour() == 0) {
|
|
return 0;
|
|
}
|
|
|
|
if (this_freq) {
|
|
this.increment_monthday(this.rule.interval);
|
|
} else {
|
|
this.increment_monthday(1);
|
|
}
|
|
|
|
return 0;
|
|
},
|
|
|
|
next_week: function next_week() {
|
|
var end_of_data = 0;
|
|
|
|
if (this.next_weekday_by_week() == 0) {
|
|
return end_of_data;
|
|
}
|
|
|
|
if (this.has_by_data("BYWEEKNO")) {
|
|
var idx = ++this.by_indices.BYWEEKNO;
|
|
|
|
if (this.by_indices.BYWEEKNO == this.by_data.BYWEEKNO.length) {
|
|
this.by_indices.BYWEEKNO = 0;
|
|
end_of_data = 1;
|
|
}
|
|
|
|
// HACK should be first month of the year
|
|
this.last.month = 1;
|
|
this.last.day = 1;
|
|
|
|
var week_no = this.by_data.BYWEEKNO[this.by_indices.BYWEEKNO];
|
|
|
|
this.last.day += 7 * week_no;
|
|
|
|
if (end_of_data) {
|
|
this.increment_year(1);
|
|
}
|
|
} else {
|
|
// Jump to the next week
|
|
this.increment_monthday(7 * this.rule.interval);
|
|
}
|
|
|
|
return end_of_data;
|
|
},
|
|
|
|
/**
|
|
* normalize each by day rule for a given year/month.
|
|
* Takes into account ordering and negative rules
|
|
*
|
|
* @param {Numeric} year current year.
|
|
* @param {Numeric} month current month.
|
|
* @param {Array} rules array of rules.
|
|
*
|
|
* @return {Array} sorted and normalized rules.
|
|
* Negative rules will be expanded to their
|
|
* correct positive values for easier processing.
|
|
*/
|
|
normalizeByMonthDayRules: function(year, month, rules) {
|
|
var daysInMonth = ICAL.Time.daysInMonth(month, year);
|
|
|
|
// XXX: This is probably bad for performance to allocate
|
|
// a new array for each month we scan, if possible
|
|
// we should try to optimize this...
|
|
var newRules = [];
|
|
|
|
var ruleIdx = 0;
|
|
var len = rules.length;
|
|
var rule;
|
|
|
|
for (; ruleIdx < len; ruleIdx++) {
|
|
rule = rules[ruleIdx];
|
|
|
|
// if this rule falls outside of given
|
|
// month discard it.
|
|
if (Math.abs(rule) > daysInMonth) {
|
|
continue;
|
|
}
|
|
|
|
// negative case
|
|
if (rule < 0) {
|
|
// we add (not subtract its a negative number)
|
|
// one from the rule because 1 === last day of month
|
|
rule = daysInMonth + (rule + 1);
|
|
} else if (rule === 0) {
|
|
// skip zero its invalid.
|
|
continue;
|
|
}
|
|
|
|
// only add unique items...
|
|
if (newRules.indexOf(rule) === -1) {
|
|
newRules.push(rule);
|
|
}
|
|
|
|
}
|
|
|
|
// unique and 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 byDay = this.by_data.BYDAY;
|
|
|
|
var date;
|
|
var dateIdx = 0;
|
|
var dateLen; // setup in initMonth
|
|
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() {
|
|
// 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();
|
|
}
|
|
|
|
initMonth();
|
|
|
|
// should come after initMonth
|
|
if (isInit) {
|
|
lastDay -= 1;
|
|
}
|
|
|
|
while (!dataIsValid) {
|
|
// 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 = lastDay + 1;
|
|
|
|
if (date > daysInMonth) {
|
|
nextMonth();
|
|
continue;
|
|
}
|
|
|
|
// find next date
|
|
var next = byMonthDay[dateIdx++];
|
|
|
|
// this logic is dependant on the BYMONTHDAYS
|
|
// being in order (which is done by #normalizeByMonthDayRules)
|
|
if (next >= date) {
|
|
// if the next month day is in the future jump to it.
|
|
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 (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.
|
|
// since dateIdx is incremented right after getting
|
|
// "next", we don't need dateLen -1 here.
|
|
if (!dataIsValid && dateIdx === dateLen) {
|
|
nextMonth();
|
|
continue;
|
|
}
|
|
}
|
|
|
|
return dataIsValid;
|
|
},
|
|
|
|
next_month: function next_month() {
|
|
var this_freq = (this.rule.freq == "MONTHLY");
|
|
var data_valid = 1;
|
|
|
|
if (this.next_hour() == 0) {
|
|
return data_valid;
|
|
}
|
|
|
|
if (this.has_by_data("BYDAY") && this.has_by_data("BYMONTHDAY")) {
|
|
data_valid = this._byDayAndMonthDay();
|
|
} else if (this.has_by_data("BYDAY")) {
|
|
var daysInMonth = ICAL.Time.daysInMonth(this.last.month, this.last.year);
|
|
var setpos = 0;
|
|
|
|
if (this.has_by_data("BYSETPOS")) {
|
|
var last_day = this.last.day;
|
|
for (var day = 1; day <= daysInMonth; day++) {
|
|
this.last.day = day;
|
|
if (this.is_day_in_byday(this.last) && day <= last_day) {
|
|
setpos++;
|
|
}
|
|
}
|
|
this.last.day = last_day;
|
|
}
|
|
|
|
for (var day = this.last.day + 1; day <= daysInMonth; day++) {
|
|
this.last.day = day;
|
|
|
|
if (this.is_day_in_byday(this.last)) {
|
|
if (!this.has_by_data("BYSETPOS") ||
|
|
this.check_set_position(++setpos) ||
|
|
this.check_set_position(setpos - this.by_data.BYSETPOS.length - 1)) {
|
|
|
|
data_valid = 1;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (day > daysInMonth) {
|
|
this.last.day = 1;
|
|
this.increment_month();
|
|
|
|
if (this.is_day_in_byday(this.last)) {
|
|
if (!this.has_by_data("BYSETPOS") || this.check_set_position(1)) {
|
|
data_valid = 1;
|
|
}
|
|
} else {
|
|
data_valid = 0;
|
|
}
|
|
}
|
|
} else if (this.has_by_data("BYMONTHDAY")) {
|
|
this.by_indices.BYMONTHDAY++;
|
|
|
|
if (this.by_indices.BYMONTHDAY >= this.by_data.BYMONTHDAY.length) {
|
|
this.by_indices.BYMONTHDAY = 0;
|
|
this.increment_month();
|
|
}
|
|
|
|
var daysInMonth = ICAL.Time.daysInMonth(this.last.month, this.last.year);
|
|
|
|
var day = this.by_data.BYMONTHDAY[this.by_indices.BYMONTHDAY];
|
|
|
|
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;
|
|
}
|
|
|
|
} 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;
|
|
},
|
|
|
|
next_weekday_by_week: function next_weekday_by_week() {
|
|
var end_of_data = 0;
|
|
|
|
if (this.next_hour() == 0) {
|
|
return end_of_data;
|
|
}
|
|
|
|
if (!this.has_by_data("BYDAY")) {
|
|
return 1;
|
|
}
|
|
|
|
for (;;) {
|
|
var tt = new ICAL.Time();
|
|
this.by_indices.BYDAY++;
|
|
|
|
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];
|
|
|
|
dow -= this.rule.wkst;
|
|
|
|
if (dow < 0) {
|
|
dow += 7;
|
|
}
|
|
|
|
tt.year = this.last.year;
|
|
tt.month = this.last.month;
|
|
tt.day = this.last.day;
|
|
|
|
var startOfWeek = tt.startDoyWeek(this.rule.wkst);
|
|
|
|
if (dow + startOfWeek < 1) {
|
|
// The selected date is in the previous year
|
|
if (!end_of_data) {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
var next = ICAL.Time.fromDayOfYear(startOfWeek + dow,
|
|
this.last.year);
|
|
|
|
/**
|
|
* The normalization horrors below are due to
|
|
* the fact that when the year/month/day changes
|
|
* it can effect the other operations that come after.
|
|
*/
|
|
this.last.year = next.year;
|
|
this.last.month = next.month;
|
|
this.last.day = next.day;
|
|
|
|
return end_of_data;
|
|
}
|
|
},
|
|
|
|
next_year: function next_year() {
|
|
|
|
if (this.next_hour() == 0) {
|
|
return 0;
|
|
}
|
|
|
|
if (++this.days_index == this.days.length) {
|
|
this.days_index = 0;
|
|
do {
|
|
this.increment_year(this.rule.interval);
|
|
this.expand_year_days(this.last.year);
|
|
} while (this.days.length == 0);
|
|
}
|
|
|
|
var next = ICAL.Time.fromDayOfYear(this.days[this.days_index],
|
|
this.last.year);
|
|
|
|
this.last.day = next.day;
|
|
this.last.month = next.month;
|
|
|
|
return 1;
|
|
},
|
|
|
|
ruleDayOfWeek: function ruleDayOfWeek(dow) {
|
|
var matches = dow.match(/([+-]?[0-9])?(MO|TU|WE|TH|FR|SA|SU)/);
|
|
if (matches) {
|
|
var pos = parseInt(matches[1] || 0, 10);
|
|
dow = ICAL.Recur.icalDayToNumericDay(matches[2]);
|
|
return [pos, dow];
|
|
} else {
|
|
return [0, 0];
|
|
}
|
|
},
|
|
|
|
next_generic: function next_generic(aRuleType, aInterval, aDateAttr,
|
|
aFollowingAttr, aPreviousIncr) {
|
|
var has_by_rule = (aRuleType in this.by_data);
|
|
var this_freq = (this.rule.freq == aInterval);
|
|
var end_of_data = 0;
|
|
|
|
if (aPreviousIncr && this[aPreviousIncr]() == 0) {
|
|
return end_of_data;
|
|
}
|
|
|
|
if (has_by_rule) {
|
|
this.by_indices[aRuleType]++;
|
|
var idx = this.by_indices[aRuleType];
|
|
var dta = this.by_data[aRuleType];
|
|
|
|
if (this.by_indices[aRuleType] == dta.length) {
|
|
this.by_indices[aRuleType] = 0;
|
|
end_of_data = 1;
|
|
}
|
|
this.last[aDateAttr] = dta[this.by_indices[aRuleType]];
|
|
} else if (this_freq) {
|
|
this["increment_" + aDateAttr](this.rule.interval);
|
|
}
|
|
|
|
if (has_by_rule && end_of_data && this_freq) {
|
|
this["increment_" + aFollowingAttr](1);
|
|
}
|
|
|
|
return end_of_data;
|
|
},
|
|
|
|
increment_monthday: function increment_monthday(inc) {
|
|
for (var i = 0; i < inc; i++) {
|
|
var daysInMonth = ICAL.Time.daysInMonth(this.last.month, this.last.year);
|
|
this.last.day++;
|
|
|
|
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 {
|
|
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);
|
|
this.last.month %= 12;
|
|
this.last.month++;
|
|
|
|
if (years != 0) {
|
|
this.increment_year(years);
|
|
}
|
|
}
|
|
},
|
|
|
|
increment_year: function increment_year(inc) {
|
|
this.last.year += inc;
|
|
},
|
|
|
|
increment_generic: function increment_generic(inc, aDateAttr,
|
|
aFactor, aNextIncrement) {
|
|
this.last[aDateAttr] += inc;
|
|
var nextunit = ICAL.helpers.trunc(this.last[aDateAttr] / aFactor);
|
|
this.last[aDateAttr] %= aFactor;
|
|
if (nextunit != 0) {
|
|
this["increment_" + aNextIncrement](nextunit);
|
|
}
|
|
},
|
|
|
|
has_by_data: function has_by_data(aRuleType) {
|
|
return (aRuleType in this.rule.parts);
|
|
},
|
|
|
|
expand_year_days: function expand_year_days(aYear) {
|
|
var t = new ICAL.Time();
|
|
this.days = [];
|
|
|
|
// We need our own copy with a few keys set
|
|
var parts = {};
|
|
var rules = ["BYDAY", "BYWEEKNO", "BYMONTHDAY", "BYMONTH", "BYYEARDAY"];
|
|
for (var p in rules) {
|
|
var part = rules[p];
|
|
if (part in this.rule.parts) {
|
|
parts[part] = this.rule.parts[part];
|
|
}
|
|
}
|
|
|
|
if ("BYMONTH" in parts && "BYWEEKNO" in parts) {
|
|
var valid = 1;
|
|
var validWeeks = {};
|
|
t.year = aYear;
|
|
t.isDate = true;
|
|
|
|
for (var monthIdx = 0; monthIdx < this.by_data.BYMONTH.length; monthIdx++) {
|
|
var month = this.by_data.BYMONTH[monthIdx];
|
|
t.month = month;
|
|
t.day = 1;
|
|
var first_week = t.weekNumber(this.rule.wkst);
|
|
t.day = ICAL.Time.daysInMonth(month, aYear);
|
|
var last_week = t.weekNumber(this.rule.wkst);
|
|
for (monthIdx = first_week; monthIdx < last_week; monthIdx++) {
|
|
validWeeks[monthIdx] = 1;
|
|
}
|
|
}
|
|
|
|
for (var weekIdx = 0; weekIdx < this.by_data.BYWEEKNO.length && valid; weekIdx++) {
|
|
var weekno = this.by_data.BYWEEKNO[weekIdx];
|
|
if (weekno < 52) {
|
|
valid &= validWeeks[weekIdx];
|
|
} else {
|
|
valid = 0;
|
|
}
|
|
}
|
|
|
|
if (valid) {
|
|
delete parts.BYMONTH;
|
|
} else {
|
|
delete parts.BYWEEKNO;
|
|
}
|
|
}
|
|
|
|
var partCount = Object.keys(parts).length;
|
|
|
|
if (partCount == 0) {
|
|
var t = this.dtstart.clone();
|
|
t.year = this.last.year;
|
|
this.days.push(t.dayOfYear());
|
|
} else if (partCount == 1 && "BYMONTH" in parts) {
|
|
for (var monthkey in this.by_data.BYMONTH) {
|
|
var t2 = this.dtstart.clone();
|
|
t2.year = aYear;
|
|
t2.month = this.by_data.BYMONTH[monthkey];
|
|
t2.isDate = true;
|
|
this.days.push(t2.dayOfYear());
|
|
}
|
|
} else if (partCount == 1 && "BYMONTHDAY" in parts) {
|
|
for (var monthdaykey in this.by_data.BYMONTHDAY) {
|
|
var t2 = this.dtstart.clone();
|
|
t2.day = this.by_data.BYMONTHDAY[monthdaykey];
|
|
t2.year = aYear;
|
|
t2.isDate = true;
|
|
this.days.push(t2.dayOfYear());
|
|
}
|
|
} else if (partCount == 2 &&
|
|
"BYMONTHDAY" in parts &&
|
|
"BYMONTH" in parts) {
|
|
for (var monthkey in this.by_data.BYMONTH) {
|
|
for (var monthdaykey in this.by_data.BYMONTHDAY) {
|
|
t.day = this.by_data.BYMONTHDAY[monthdaykey];
|
|
t.month = this.by_data.BYMONTH[monthkey];
|
|
t.year = aYear;
|
|
t.isDate = true;
|
|
|
|
this.days.push(t.dayOfYear());
|
|
}
|
|
}
|
|
} else if (partCount == 1 && "BYWEEKNO" in parts) {
|
|
// TODO unimplemented in libical
|
|
} else if (partCount == 2 &&
|
|
"BYWEEKNO" in parts &&
|
|
"BYMONTHDAY" in parts) {
|
|
// TODO unimplemented in libical
|
|
} else if (partCount == 1 && "BYDAY" in parts) {
|
|
this.days = this.days.concat(this.expand_by_day(aYear));
|
|
} else if (partCount == 2 && "BYDAY" in parts && "BYMONTH" in parts) {
|
|
for (var monthkey in this.by_data.BYMONTH) {
|
|
month = this.by_data.BYMONTH[monthkey];
|
|
var daysInMonth = ICAL.Time.daysInMonth(month, aYear);
|
|
|
|
t.year = aYear;
|
|
t.month = this.by_data.BYMONTH[monthkey];
|
|
t.day = 1;
|
|
t.isDate = true;
|
|
|
|
var first_dow = t.dayOfWeek();
|
|
var doy_offset = t.dayOfYear() - 1;
|
|
|
|
t.day = daysInMonth;
|
|
var last_dow = t.dayOfWeek();
|
|
|
|
if (this.has_by_data("BYSETPOS")) {
|
|
var set_pos_counter = 0;
|
|
var by_month_day = [];
|
|
for (var day = 1; day <= daysInMonth; day++) {
|
|
t.day = day;
|
|
if (this.is_day_in_byday(t)) {
|
|
by_month_day.push(day);
|
|
}
|
|
}
|
|
|
|
for (var spIndex = 0; spIndex < by_month_day.length; spIndex++) {
|
|
if (this.check_set_position(spIndex + 1) ||
|
|
this.check_set_position(spIndex - by_month_day.length)) {
|
|
this.days.push(doy_offset + by_month_day[spIndex]);
|
|
}
|
|
}
|
|
} else {
|
|
for (var daycodedkey in this.by_data.BYDAY) {
|
|
//TODO: This should return dates in order of occurrence
|
|
// (1,2,3, etc...) instead of by weekday (su, mo, etc..)
|
|
var coded_day = this.by_data.BYDAY[daycodedkey];
|
|
var parts = this.ruleDayOfWeek(coded_day);
|
|
var pos = parts[0];
|
|
var dow = parts[1];
|
|
var month_day;
|
|
|
|
var first_matching_day = ((dow + 7 - first_dow) % 7) + 1;
|
|
var last_matching_day = daysInMonth - ((last_dow + 7 - dow) % 7);
|
|
|
|
if (pos == 0) {
|
|
for (var day = first_matching_day; day <= daysInMonth; day += 7) {
|
|
this.days.push(doy_offset + day);
|
|
}
|
|
} else if (pos > 0) {
|
|
month_day = first_matching_day + (pos - 1) * 7;
|
|
|
|
if (month_day <= daysInMonth) {
|
|
this.days.push(doy_offset + month_day);
|
|
}
|
|
} else {
|
|
month_day = last_matching_day + (pos + 1) * 7;
|
|
|
|
if (month_day > 0) {
|
|
this.days.push(doy_offset + month_day);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else if (partCount == 2 && "BYDAY" in parts && "BYMONTHDAY" in parts) {
|
|
var expandedDays = this.expand_by_day(aYear);
|
|
|
|
for (var daykey in expandedDays) {
|
|
var day = expandedDays[daykey];
|
|
var tt = ICAL.Time.fromDayOfYear(day, aYear);
|
|
if (this.by_data.BYMONTHDAY.indexOf(tt.day) >= 0) {
|
|
this.days.push(day);
|
|
}
|
|
}
|
|
} else if (partCount == 3 &&
|
|
"BYDAY" in parts &&
|
|
"BYMONTHDAY" in parts &&
|
|
"BYMONTH" in parts) {
|
|
var expandedDays = this.expand_by_day(aYear);
|
|
|
|
for (var daykey in expandedDays) {
|
|
var day = expandedDays[daykey];
|
|
var tt = ICAL.Time.fromDayOfYear(day, aYear);
|
|
|
|
if (this.by_data.BYMONTH.indexOf(tt.month) >= 0 &&
|
|
this.by_data.BYMONTHDAY.indexOf(tt.day) >= 0) {
|
|
this.days.push(day);
|
|
}
|
|
}
|
|
} else if (partCount == 2 && "BYDAY" in parts && "BYWEEKNO" in parts) {
|
|
var expandedDays = this.expand_by_day(aYear);
|
|
|
|
for (var daykey in expandedDays) {
|
|
var day = expandedDays[daykey];
|
|
var tt = ICAL.Time.fromDayOfYear(day, aYear);
|
|
var weekno = tt.weekNumber(this.rule.wkst);
|
|
|
|
if (this.by_data.BYWEEKNO.indexOf(weekno)) {
|
|
this.days.push(day);
|
|
}
|
|
}
|
|
} else if (partCount == 3 &&
|
|
"BYDAY" in parts &&
|
|
"BYWEEKNO" in parts &&
|
|
"BYMONTHDAY" in parts) {
|
|
// TODO unimplemted in libical
|
|
} else if (partCount == 1 && "BYYEARDAY" in parts) {
|
|
this.days = this.days.concat(this.by_data.BYYEARDAY);
|
|
} else {
|
|
this.days = [];
|
|
}
|
|
return 0;
|
|
},
|
|
|
|
expand_by_day: function expand_by_day(aYear) {
|
|
|
|
var days_list = [];
|
|
var tmp = this.last.clone();
|
|
|
|
tmp.year = aYear;
|
|
tmp.month = 1;
|
|
tmp.day = 1;
|
|
tmp.isDate = true;
|
|
|
|
var start_dow = tmp.dayOfWeek();
|
|
|
|
tmp.month = 12;
|
|
tmp.day = 31;
|
|
tmp.isDate = true;
|
|
|
|
var end_dow = tmp.dayOfWeek();
|
|
var end_year_day = tmp.dayOfYear();
|
|
|
|
for (var daykey in this.by_data.BYDAY) {
|
|
var day = this.by_data.BYDAY[daykey];
|
|
var parts = this.ruleDayOfWeek(day);
|
|
var pos = parts[0];
|
|
var dow = parts[1];
|
|
|
|
if (pos == 0) {
|
|
var tmp_start_doy = ((dow + 7 - start_dow) % 7) + 1;
|
|
|
|
for (var doy = tmp_start_doy; doy <= end_year_day; doy += 7) {
|
|
days_list.push(doy);
|
|
}
|
|
|
|
} else if (pos > 0) {
|
|
var first;
|
|
if (dow >= start_dow) {
|
|
first = dow - start_dow + 1;
|
|
} else {
|
|
first = dow - start_dow + 8;
|
|
}
|
|
|
|
days_list.push(first + (pos - 1) * 7);
|
|
} else {
|
|
var last;
|
|
pos = -pos;
|
|
|
|
if (dow <= end_dow) {
|
|
last = end_year_day - end_dow + dow;
|
|
} else {
|
|
last = end_year_day - end_dow + dow - 7;
|
|
}
|
|
|
|
days_list.push(last - (pos - 1) * 7);
|
|
}
|
|
}
|
|
return days_list;
|
|
},
|
|
|
|
is_day_in_byday: function is_day_in_byday(tt) {
|
|
for (var daykey in this.by_data.BYDAY) {
|
|
var day = this.by_data.BYDAY[daykey];
|
|
var parts = this.ruleDayOfWeek(day);
|
|
var pos = parts[0];
|
|
var dow = parts[1];
|
|
var this_dow = tt.dayOfWeek();
|
|
|
|
if ((pos == 0 && dow == this_dow) ||
|
|
(tt.nthWeekDay(dow, pos) == tt.day)) {
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
},
|
|
|
|
/**
|
|
* Checks if given value is in BYSETPOS.
|
|
*
|
|
* @param {Numeric} aPos position to check for.
|
|
* @return {Boolean} false unless BYSETPOS rules exist
|
|
* and the given value is present in rules.
|
|
*/
|
|
check_set_position: function check_set_position(aPos) {
|
|
if (this.has_by_data('BYSETPOS')) {
|
|
var idx = this.by_data.BYSETPOS.indexOf(aPos);
|
|
// negative numbers are not false-y
|
|
return idx !== -1;
|
|
}
|
|
return false;
|
|
},
|
|
|
|
sort_byday_rules: function icalrecur_sort_byday_rules(aRules, aWeekStart) {
|
|
for (var i = 0; i < aRules.length; i++) {
|
|
for (var j = 0; j < i; j++) {
|
|
var one = this.ruleDayOfWeek(aRules[j])[1];
|
|
var two = this.ruleDayOfWeek(aRules[i])[1];
|
|
one -= aWeekStart;
|
|
two -= aWeekStart;
|
|
if (one < 0) one += 7;
|
|
if (two < 0) two += 7;
|
|
|
|
if (one > two) {
|
|
var tmp = aRules[i];
|
|
aRules[i] = aRules[j];
|
|
aRules[j] = tmp;
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
check_contract_restriction: function check_contract_restriction(aRuleType, v) {
|
|
var indexMapValue = icalrecur_iterator._indexMap[aRuleType];
|
|
var ruleMapValue = icalrecur_iterator._expandMap[this.rule.freq][indexMapValue];
|
|
var pass = false;
|
|
|
|
if (aRuleType in this.by_data &&
|
|
ruleMapValue == icalrecur_iterator.CONTRACT) {
|
|
|
|
var ruleType = this.by_data[aRuleType];
|
|
|
|
for (var bydatakey in ruleType) {
|
|
if (ruleType[bydatakey] == v) {
|
|
pass = true;
|
|
break;
|
|
}
|
|
}
|
|
} else {
|
|
// Not a contracting byrule or has no data, test passes
|
|
pass = true;
|
|
}
|
|
return pass;
|
|
},
|
|
|
|
check_contracting_rules: function check_contracting_rules() {
|
|
var dow = this.last.dayOfWeek();
|
|
var weekNo = this.last.weekNumber(this.rule.wkst);
|
|
var doy = this.last.dayOfYear();
|
|
|
|
return (this.check_contract_restriction("BYSECOND", this.last.second) &&
|
|
this.check_contract_restriction("BYMINUTE", this.last.minute) &&
|
|
this.check_contract_restriction("BYHOUR", this.last.hour) &&
|
|
this.check_contract_restriction("BYDAY", ICAL.Recur.numericDayToIcalDay(dow)) &&
|
|
this.check_contract_restriction("BYWEEKNO", weekNo) &&
|
|
this.check_contract_restriction("BYMONTHDAY", this.last.day) &&
|
|
this.check_contract_restriction("BYMONTH", this.last.month) &&
|
|
this.check_contract_restriction("BYYEARDAY", doy));
|
|
},
|
|
|
|
setup_defaults: function setup_defaults(aRuleType, req, deftime) {
|
|
var indexMapValue = icalrecur_iterator._indexMap[aRuleType];
|
|
var ruleMapValue = icalrecur_iterator._expandMap[this.rule.freq][indexMapValue];
|
|
|
|
if (ruleMapValue != icalrecur_iterator.CONTRACT) {
|
|
if (!(aRuleType in this.by_data)) {
|
|
this.by_data[aRuleType] = [deftime];
|
|
}
|
|
if (this.rule.freq != req) {
|
|
return this.by_data[aRuleType][0];
|
|
}
|
|
}
|
|
return deftime;
|
|
},
|
|
|
|
/**
|
|
* Convert iterator into a serialize-able object.
|
|
* Will preserve current iteration sequence to ensure
|
|
* the seamless continuation of the recurrence rule.
|
|
*/
|
|
toJSON: function() {
|
|
var result = Object.create(null);
|
|
|
|
result.initialized = this.initialized;
|
|
result.rule = this.rule.toJSON();
|
|
result.dtstart = this.dtstart.toJSON();
|
|
result.by_data = this.by_data;
|
|
result.days = this.days;
|
|
result.last = this.last.toJSON();
|
|
result.by_indices = this.by_indices;
|
|
result.occurrence_number = this.occurrence_number;
|
|
|
|
return result;
|
|
}
|
|
|
|
};
|
|
|
|
icalrecur_iterator._indexMap = {
|
|
"BYSECOND": 0,
|
|
"BYMINUTE": 1,
|
|
"BYHOUR": 2,
|
|
"BYDAY": 3,
|
|
"BYMONTHDAY": 4,
|
|
"BYYEARDAY": 5,
|
|
"BYWEEKNO": 6,
|
|
"BYMONTH": 7,
|
|
"BYSETPOS": 8
|
|
};
|
|
|
|
icalrecur_iterator._expandMap = {
|
|
"SECONDLY": [1, 1, 1, 1, 1, 1, 1, 1],
|
|
"MINUTELY": [2, 1, 1, 1, 1, 1, 1, 1],
|
|
"HOURLY": [2, 2, 1, 1, 1, 1, 1, 1],
|
|
"DAILY": [2, 2, 2, 1, 1, 1, 1, 1],
|
|
"WEEKLY": [2, 2, 2, 2, 3, 3, 1, 1],
|
|
"MONTHLY": [2, 2, 2, 2, 2, 3, 3, 1],
|
|
"YEARLY": [2, 2, 2, 2, 2, 2, 2, 2]
|
|
};
|
|
icalrecur_iterator.UNKNOWN = 0;
|
|
icalrecur_iterator.CONTRACT = 1;
|
|
icalrecur_iterator.EXPAND = 2;
|
|
icalrecur_iterator.ILLEGAL = 3;
|
|
|
|
return icalrecur_iterator;
|
|
|
|
}());
|
|
ICAL.RecurExpansion = (function() {
|
|
function formatTime(item) {
|
|
return ICAL.helpers.formatClassType(item, ICAL.Time);
|
|
}
|
|
|
|
function compareTime(a, b) {
|
|
return a.compare(b);
|
|
}
|
|
|
|
function isRecurringComponent(comp) {
|
|
return comp.hasProperty('rdate') ||
|
|
comp.hasProperty('rrule') ||
|
|
comp.hasProperty('recurrence-id');
|
|
}
|
|
|
|
/**
|
|
* Primary class for expanding recurring rules.
|
|
* Can take multiple rrules, rdates, exdate(s)
|
|
* and iterate (in order) over each next occurrence.
|
|
*
|
|
* Once initialized this class can also be serialized
|
|
* saved and continue iteration from the last point.
|
|
*
|
|
* NOTE: it is intended that this class is to be used
|
|
* with ICAL.Event which handles recurrence exceptions.
|
|
*
|
|
* Options:
|
|
* - dtstart: (ICAL.Time) start time of event (required)
|
|
* - component: (ICAL.Component) component (required unless resuming)
|
|
*
|
|
* Examples:
|
|
*
|
|
* // assuming event is a parsed ical component
|
|
* var event;
|
|
*
|
|
* var expand = new ICAL.RecurExpansion({
|
|
* component: event,
|
|
* start: event.getFirstPropertyValue('DTSTART')
|
|
* });
|
|
*
|
|
* // remember there are infinite rules
|
|
* // so its a good idea to limit the scope
|
|
* // of the iterations then resume later on.
|
|
*
|
|
* // next is always an ICAL.Time or null
|
|
* var next;
|
|
*
|
|
* while(someCondition && (next = expand.next())) {
|
|
* // do something with next
|
|
* }
|
|
*
|
|
* // save instance for later
|
|
* var json = JSON.stringify(expand);
|
|
*
|
|
* //...
|
|
*
|
|
* // NOTE: if the component's properties have
|
|
* // changed you will need to rebuild the
|
|
* // class and start over. This only works
|
|
* // when the component's recurrence info is the same.
|
|
* var expand = new ICAL.RecurExpansion(JSON.parse(json));
|
|
*
|
|
*
|
|
* @param {Object} options see options block.
|
|
*/
|
|
function RecurExpansion(options) {
|
|
this.ruleDates = [];
|
|
this.exDates = [];
|
|
this.fromData(options);
|
|
}
|
|
|
|
RecurExpansion.prototype = {
|
|
|
|
/**
|
|
* True when iteration is fully completed.
|
|
*/
|
|
complete: false,
|
|
|
|
/**
|
|
* Array of rrule iterators.
|
|
*
|
|
* @type Array[ICAL.RecurIterator]
|
|
* @private
|
|
*/
|
|
ruleIterators: null,
|
|
|
|
/**
|
|
* Array of rdate instances.
|
|
*
|
|
* @type Array[ICAL.Time]
|
|
* @private
|
|
*/
|
|
ruleDates: null,
|
|
|
|
/**
|
|
* Array of exdate instances.
|
|
*
|
|
* @type Array[ICAL.Time]
|
|
* @private
|
|
*/
|
|
exDates: null,
|
|
|
|
/**
|
|
* Current position in ruleDates array.
|
|
* @type Numeric
|
|
* @private
|
|
*/
|
|
ruleDateInc: 0,
|
|
|
|
/**
|
|
* Current position in exDates array
|
|
* @type Numeric
|
|
* @private
|
|
*/
|
|
exDateInc: 0,
|
|
|
|
/**
|
|
* Current negative date.
|
|
*
|
|
* @type ICAL.Time
|
|
* @private
|
|
*/
|
|
exDate: null,
|
|
|
|
/**
|
|
* Current additional date.
|
|
*
|
|
* @type ICAL.Time
|
|
* @private
|
|
*/
|
|
ruleDate: null,
|
|
|
|
/**
|
|
* Start date of recurring rules.
|
|
*
|
|
* @type ICAL.Time
|
|
*/
|
|
dtstart: null,
|
|
|
|
/**
|
|
* Last expanded time
|
|
*
|
|
* @type ICAL.Time
|
|
*/
|
|
last: null,
|
|
|
|
fromData: function(options) {
|
|
var start = ICAL.helpers.formatClassType(options.dtstart, ICAL.Time);
|
|
|
|
if (!start) {
|
|
throw new Error('.dtstart (ICAL.Time) must be given');
|
|
} else {
|
|
this.dtstart = start;
|
|
}
|
|
|
|
if (options.component) {
|
|
this._init(options.component);
|
|
} else {
|
|
this.last = formatTime(options.last);
|
|
|
|
this.ruleIterators = options.ruleIterators.map(function(item) {
|
|
return ICAL.helpers.formatClassType(item, ICAL.RecurIterator);
|
|
});
|
|
|
|
this.ruleDateInc = options.ruleDateInc;
|
|
this.exDateInc = options.exDateInc;
|
|
|
|
if (options.ruleDates) {
|
|
this.ruleDates = options.ruleDates.map(formatTime);
|
|
this.ruleDate = this.ruleDates[this.ruleDateInc];
|
|
}
|
|
|
|
if (options.exDates) {
|
|
this.exDates = options.exDates.map(formatTime);
|
|
this.exDate = this.exDates[this.exDateInc];
|
|
}
|
|
|
|
if (typeof(options.complete) !== 'undefined') {
|
|
this.complete = options.complete;
|
|
}
|
|
}
|
|
},
|
|
|
|
next: function() {
|
|
var iter;
|
|
var ruleOfDay;
|
|
var next;
|
|
var compare;
|
|
|
|
var maxTries = 500;
|
|
var currentTry = 0;
|
|
|
|
while (true) {
|
|
if (currentTry++ > maxTries) {
|
|
throw new Error(
|
|
'max tries have occured, rule may be impossible to forfill.'
|
|
);
|
|
}
|
|
|
|
next = this.ruleDate;
|
|
iter = this._nextRecurrenceIter(this.last);
|
|
|
|
// no more matches
|
|
// because we increment the rule day or rule
|
|
// _after_ we choose a value this should be
|
|
// the only spot where we need to worry about the
|
|
// end of events.
|
|
if (!next && !iter) {
|
|
// there are no more iterators or rdates
|
|
this.complete = true;
|
|
break;
|
|
}
|
|
|
|
// no next rule day or recurrence rule is first.
|
|
if (!next || (iter && next.compare(iter.last) > 0)) {
|
|
// must be cloned, recur will reuse the time element.
|
|
next = iter.last.clone();
|
|
// move to next so we can continue
|
|
iter.next();
|
|
}
|
|
|
|
// if the ruleDate is still next increment it.
|
|
if (this.ruleDate === next) {
|
|
this._nextRuleDay();
|
|
}
|
|
|
|
this.last = next;
|
|
|
|
// check the negative rules
|
|
if (this.exDate) {
|
|
compare = this.exDate.compare(this.last);
|
|
|
|
if (compare < 0) {
|
|
this._nextExDay();
|
|
}
|
|
|
|
// if the current rule is excluded skip it.
|
|
if (compare === 0) {
|
|
this._nextExDay();
|
|
continue;
|
|
}
|
|
}
|
|
|
|
//XXX: The spec states that after we resolve the final
|
|
// list of dates we execute exdate this seems somewhat counter
|
|
// intuitive to what I have seen most servers do so for now
|
|
// I exclude based on the original date not the one that may
|
|
// have been modified by the exception.
|
|
return this.last;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Converts object into a serialize-able format.
|
|
*/
|
|
toJSON: function() {
|
|
function toJSON(item) {
|
|
return item.toJSON();
|
|
}
|
|
|
|
var result = Object.create(null);
|
|
result.ruleIterators = this.ruleIterators.map(toJSON);
|
|
|
|
if (this.ruleDates) {
|
|
result.ruleDates = this.ruleDates.map(toJSON);
|
|
}
|
|
|
|
if (this.exDates) {
|
|
result.exDates = this.exDates.map(toJSON);
|
|
}
|
|
|
|
result.ruleDateInc = this.ruleDateInc;
|
|
result.exDateInc = this.exDateInc;
|
|
result.last = this.last.toJSON();
|
|
result.dtstart = this.dtstart.toJSON();
|
|
result.complete = this.complete;
|
|
|
|
return result;
|
|
},
|
|
|
|
|
|
_extractDates: function(component, property) {
|
|
var result = [];
|
|
var props = component.getAllProperties(property);
|
|
var len = props.length;
|
|
var i = 0;
|
|
var prop;
|
|
|
|
var idx;
|
|
|
|
for (; i < len; i++) {
|
|
props[i].getValues().forEach(function(prop) {
|
|
idx = ICAL.helpers.binsearchInsert(
|
|
result,
|
|
prop,
|
|
compareTime
|
|
);
|
|
|
|
// ordered insert
|
|
result.splice(idx, 0, prop);
|
|
});
|
|
}
|
|
|
|
return result;
|
|
},
|
|
|
|
_init: function(component) {
|
|
this.ruleIterators = [];
|
|
|
|
this.last = this.dtstart.clone();
|
|
|
|
// to provide api consistency non-recurring
|
|
// events can also use the iterator though it will
|
|
// only return a single time.
|
|
if (!isRecurringComponent(component)) {
|
|
this.ruleDate = this.last.clone();
|
|
this.complete = true;
|
|
return;
|
|
}
|
|
|
|
if (component.hasProperty('rdate')) {
|
|
this.ruleDates = this._extractDates(component, 'rdate');
|
|
|
|
// special hack for cases where first rdate is prior
|
|
// to the start date. We only check for the first rdate.
|
|
// This is mostly for google's crazy recurring date logic
|
|
// (contacts birthdays).
|
|
if ((this.ruleDates[0]) &&
|
|
(this.ruleDates[0].compare(this.dtstart) < 0)) {
|
|
|
|
this.ruleDateInc = 0;
|
|
this.last = this.ruleDates[0].clone();
|
|
} else {
|
|
this.ruleDateInc = ICAL.helpers.binsearchInsert(
|
|
this.ruleDates,
|
|
this.last,
|
|
compareTime
|
|
);
|
|
}
|
|
|
|
this.ruleDate = this.ruleDates[this.ruleDateInc];
|
|
}
|
|
|
|
if (component.hasProperty('rrule')) {
|
|
var rules = component.getAllProperties('rrule');
|
|
var i = 0;
|
|
var len = rules.length;
|
|
|
|
var rule;
|
|
var iter;
|
|
|
|
for (; i < len; i++) {
|
|
rule = rules[i].getFirstValue();
|
|
iter = rule.iterator(this.dtstart);
|
|
this.ruleIterators.push(iter);
|
|
|
|
// increment to the next occurrence so future
|
|
// calls to next return times beyond the initial iteration.
|
|
// XXX: I find this suspicious might be a bug?
|
|
iter.next();
|
|
}
|
|
}
|
|
|
|
if (component.hasProperty('exdate')) {
|
|
this.exDates = this._extractDates(component, 'exdate');
|
|
// if we have a .last day we increment the index to beyond it.
|
|
this.exDateInc = ICAL.helpers.binsearchInsert(
|
|
this.exDates,
|
|
this.last,
|
|
compareTime
|
|
);
|
|
|
|
this.exDate = this.exDates[this.exDateInc];
|
|
}
|
|
},
|
|
|
|
_nextExDay: function() {
|
|
this.exDate = this.exDates[++this.exDateInc];
|
|
},
|
|
|
|
_nextRuleDay: function() {
|
|
this.ruleDate = this.ruleDates[++this.ruleDateInc];
|
|
},
|
|
|
|
/**
|
|
* Find and return the recurrence rule with the most
|
|
* recent event and return it.
|
|
*
|
|
* @return {Object} iterator.
|
|
*/
|
|
_nextRecurrenceIter: function() {
|
|
var iters = this.ruleIterators;
|
|
|
|
if (iters.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
var len = iters.length;
|
|
var iter;
|
|
var iterTime;
|
|
var iterIdx = 0;
|
|
var chosenIter;
|
|
|
|
// loop through each iterator
|
|
for (; iterIdx < len; iterIdx++) {
|
|
iter = iters[iterIdx];
|
|
iterTime = iter.last;
|
|
|
|
// if iteration is complete
|
|
// then we must exclude it from
|
|
// the search and remove it.
|
|
if (iter.completed) {
|
|
len--;
|
|
if (iterIdx !== 0) {
|
|
iterIdx--;
|
|
}
|
|
iters.splice(iterIdx, 1);
|
|
continue;
|
|
}
|
|
|
|
// find the most recent possible choice
|
|
if (!chosenIter || chosenIter.last.compare(iterTime) > 0) {
|
|
// that iterator is saved
|
|
chosenIter = iter;
|
|
}
|
|
}
|
|
|
|
// the chosen iterator is returned but not mutated
|
|
// this iterator contains the most recent event.
|
|
return chosenIter;
|
|
}
|
|
|
|
};
|
|
|
|
return RecurExpansion;
|
|
|
|
}());
|
|
ICAL.Event = (function() {
|
|
|
|
function compareRangeException(a, b) {
|
|
if (a[0] > b[0]) return 1;
|
|
if (b[0] > a[0]) return -1;
|
|
return 0;
|
|
}
|
|
|
|
function Event(component, options) {
|
|
if (!(component instanceof ICAL.Component)) {
|
|
options = component;
|
|
component = null;
|
|
}
|
|
|
|
if (component) {
|
|
this.component = component;
|
|
} else {
|
|
this.component = new ICAL.Component('vevent');
|
|
}
|
|
|
|
this._rangeExceptionCache = Object.create(null);
|
|
this.exceptions = Object.create(null);
|
|
this.rangeExceptions = [];
|
|
|
|
if (options && options.strictExceptions) {
|
|
this.strictExceptions = options.strictExceptions;
|
|
}
|
|
|
|
if (options && options.exceptions) {
|
|
options.exceptions.forEach(this.relateException, this);
|
|
}
|
|
}
|
|
|
|
Event.prototype = {
|
|
|
|
THISANDFUTURE: 'THISANDFUTURE',
|
|
|
|
/**
|
|
* List of related event exceptions.
|
|
*
|
|
* @type Array[ICAL.Event]
|
|
*/
|
|
exceptions: null,
|
|
|
|
/**
|
|
* When true will verify exceptions are related by their UUID.
|
|
*
|
|
* @type {Boolean}
|
|
*/
|
|
strictExceptions: false,
|
|
|
|
/**
|
|
* Relates a given event exception to this object.
|
|
* If the given component does not share the UID of
|
|
* this event it cannot be related and will throw an
|
|
* exception.
|
|
*
|
|
* If this component is an exception it cannot have other
|
|
* exceptions related to it.
|
|
*
|
|
* @param {ICAL.Component|ICAL.Event} obj component or event.
|
|
*/
|
|
relateException: function(obj) {
|
|
if (this.isRecurrenceException()) {
|
|
throw new Error('cannot relate exception to exceptions');
|
|
}
|
|
|
|
if (obj instanceof ICAL.Component) {
|
|
obj = new ICAL.Event(obj);
|
|
}
|
|
|
|
if (this.strictExceptions && obj.uid !== this.uid) {
|
|
throw new Error('attempted to relate unrelated exception');
|
|
}
|
|
|
|
var id = obj.recurrenceId.toString();
|
|
|
|
// we don't sort or manage exceptions directly
|
|
// here the recurrence expander handles that.
|
|
this.exceptions[id] = obj;
|
|
|
|
// index RANGE=THISANDFUTURE exceptions so we can
|
|
// look them up later in getOccurrenceDetails.
|
|
if (obj.modifiesFuture()) {
|
|
var item = [
|
|
obj.recurrenceId.toUnixTime(), id
|
|
];
|
|
|
|
// we keep them sorted so we can find the nearest
|
|
// value later on...
|
|
var idx = ICAL.helpers.binsearchInsert(
|
|
this.rangeExceptions,
|
|
item,
|
|
compareRangeException
|
|
);
|
|
|
|
this.rangeExceptions.splice(idx, 0, item);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* If this record is an exception and has the RANGE=THISANDFUTURE value.
|
|
*
|
|
* @return {Boolean} true when is exception with range.
|
|
*/
|
|
modifiesFuture: function() {
|
|
var range = this.component.getFirstPropertyValue('range');
|
|
return range === this.THISANDFUTURE;
|
|
},
|
|
|
|
/**
|
|
* Finds the range exception nearest to the given date.
|
|
*
|
|
* @param {ICAL.Time} time usually an occurrence time of an event.
|
|
* @return {ICAL.Event|Null} the related event/exception or null.
|
|
*/
|
|
findRangeException: function(time) {
|
|
if (!this.rangeExceptions.length) {
|
|
return null;
|
|
}
|
|
|
|
var utc = time.toUnixTime();
|
|
var idx = ICAL.helpers.binsearchInsert(
|
|
this.rangeExceptions,
|
|
[utc],
|
|
compareRangeException
|
|
);
|
|
|
|
idx -= 1;
|
|
|
|
// occurs before
|
|
if (idx < 0) {
|
|
return null;
|
|
}
|
|
|
|
var rangeItem = this.rangeExceptions[idx];
|
|
|
|
// sanity check
|
|
if (utc < rangeItem[0]) {
|
|
return null;
|
|
}
|
|
|
|
return rangeItem[1];
|
|
},
|
|
|
|
/**
|
|
* Returns the occurrence details based on its start time.
|
|
* If the occurrence has an exception will return the details
|
|
* for that exception.
|
|
*
|
|
* 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
|
|
);
|
|
|
|
if (rangeExceptionId) {
|
|
var exception = this.exceptions[rangeExceptionId];
|
|
|
|
// range exception must modify standard time
|
|
// by the difference (if any) in start/end times.
|
|
result.item = exception;
|
|
|
|
var startDiff = this._rangeExceptionCache[rangeExceptionId];
|
|
|
|
if (!startDiff) {
|
|
var original = exception.recurrenceId.clone();
|
|
var newStart = exception.startDate.clone();
|
|
|
|
// zones must be same otherwise subtract may be incorrect.
|
|
original.zone = newStart.zone;
|
|
var startDiff = newStart.subtractDate(original);
|
|
|
|
this._rangeExceptionCache[rangeExceptionId] = startDiff;
|
|
}
|
|
|
|
var start = occurrence.clone();
|
|
start.zone = exception.startDate.zone;
|
|
start.addDuration(startDiff);
|
|
|
|
var end = start.clone();
|
|
end.addDuration(exception.duration);
|
|
|
|
result.startDate = start;
|
|
result.endDate = end;
|
|
} else {
|
|
// no range exception standard expansion
|
|
var end = occurrence.clone();
|
|
end.addDuration(this.duration);
|
|
|
|
result.endDate = end;
|
|
result.startDate = occurrence;
|
|
result.item = this;
|
|
}
|
|
}
|
|
|
|
return result;
|
|
},
|
|
|
|
/**
|
|
* Builds a recur expansion instance for a specific
|
|
* point in time (defaults to startDate).
|
|
*
|
|
* @return {ICAL.RecurExpansion} expander object.
|
|
*/
|
|
iterator: function(startTime) {
|
|
return new ICAL.RecurExpansion({
|
|
component: this.component,
|
|
dtstart: startTime || this.startDate
|
|
});
|
|
},
|
|
|
|
isRecurring: function() {
|
|
var comp = this.component;
|
|
return comp.hasProperty('rrule') || comp.hasProperty('rdate');
|
|
},
|
|
|
|
isRecurrenceException: function() {
|
|
return this.component.hasProperty('recurrence-id');
|
|
},
|
|
|
|
/**
|
|
* Returns the types of recurrences this event may have.
|
|
*
|
|
* Returned as an object with the following possible keys:
|
|
*
|
|
* - YEARLY
|
|
* - MONTHLY
|
|
* - WEEKLY
|
|
* - DAILY
|
|
* - MINUTELY
|
|
* - SECONDLY
|
|
*
|
|
* @return {Object} object of recurrence flags.
|
|
*/
|
|
getRecurrenceTypes: function() {
|
|
var rules = this.component.getAllProperties('rrule');
|
|
var i = 0;
|
|
var len = rules.length;
|
|
var result = Object.create(null);
|
|
|
|
for (; i < len; i++) {
|
|
var value = rules[i].getFirstValue();
|
|
result[value.freq] = true;
|
|
}
|
|
|
|
return result;
|
|
},
|
|
|
|
get uid() {
|
|
return this._firstProp('uid');
|
|
},
|
|
|
|
set uid(value) {
|
|
this._setProp('uid', value);
|
|
},
|
|
|
|
get startDate() {
|
|
return this._firstProp('dtstart');
|
|
},
|
|
|
|
set startDate(value) {
|
|
this._setTime('dtstart', value);
|
|
},
|
|
|
|
get endDate() {
|
|
return this._firstProp('dtend');
|
|
},
|
|
|
|
set endDate(value) {
|
|
this._setTime('dtend', value);
|
|
},
|
|
|
|
get duration() {
|
|
return this.endDate.subtractDate(this.startDate);
|
|
},
|
|
|
|
get location() {
|
|
return this._firstProp('location');
|
|
},
|
|
|
|
set location(value) {
|
|
return this._setProp('location', value);
|
|
},
|
|
|
|
get attendees() {
|
|
//XXX: This is way lame we should have a better
|
|
// data structure for this later.
|
|
return this.component.getAllProperties('attendee');
|
|
},
|
|
|
|
get summary() {
|
|
return this._firstProp('summary');
|
|
},
|
|
|
|
set summary(value) {
|
|
this._setProp('summary', value);
|
|
},
|
|
|
|
get description() {
|
|
return this._firstProp('description');
|
|
},
|
|
|
|
set description(value) {
|
|
this._setProp('description', value);
|
|
},
|
|
|
|
get organizer() {
|
|
return this._firstProp('organizer');
|
|
},
|
|
|
|
set organizer(value) {
|
|
this._setProp('organizer', value);
|
|
},
|
|
|
|
get sequence() {
|
|
return this._firstProp('sequence');
|
|
},
|
|
|
|
set sequence(value) {
|
|
this._setProp('sequence', value);
|
|
},
|
|
|
|
get recurrenceId() {
|
|
return this._firstProp('recurrence-id');
|
|
},
|
|
|
|
set recurrenceId(value) {
|
|
this._setProp('recurrence-id', value);
|
|
},
|
|
|
|
/**
|
|
* set/update a time property's value.
|
|
* This will also update the TZID of the property.
|
|
*
|
|
* TODO: this method handles the case where we are switching
|
|
* from a known timezone to an implied timezone (one without TZID).
|
|
* This does _not_ handle the case of moving between a known
|
|
* (by TimezoneService) timezone to an unknown timezone...
|
|
*
|
|
* We will not add/remove/update the VTIMEZONE subcomponents
|
|
* leading to invalid ICAL data...
|
|
*/
|
|
_setTime: function(propName, time) {
|
|
var prop = this.component.getFirstProperty(propName);
|
|
|
|
if (!prop) {
|
|
prop = new ICAL.Property(propName);
|
|
this.component.addProperty(prop);
|
|
}
|
|
|
|
// utc and local don't get a tzid
|
|
if (
|
|
time.zone === ICAL.Timezone.localTimezone ||
|
|
time.zone === ICAL.Timezone.utcTimezone
|
|
) {
|
|
// remove the tzid
|
|
prop.removeParameter('tzid');
|
|
} else {
|
|
prop.setParameter('tzid', time.zone.tzid);
|
|
}
|
|
|
|
prop.setValue(time);
|
|
},
|
|
|
|
_setProp: function(name, value) {
|
|
this.component.updatePropertyWithValue(name, value);
|
|
},
|
|
|
|
_firstProp: function(name) {
|
|
return this.component.getFirstPropertyValue(name);
|
|
},
|
|
|
|
toString: function() {
|
|
return this.component.toString();
|
|
}
|
|
|
|
};
|
|
|
|
return Event;
|
|
|
|
}());
|
|
ICAL.ComponentParser = (function() {
|
|
|
|
/**
|
|
* Component parser initializer.
|
|
*
|
|
* Usage:
|
|
*
|
|
* var options = {
|
|
* // when false no events will be emitted for type
|
|
* parseEvent: true,
|
|
* parseTimezone: true
|
|
* };
|
|
*
|
|
* var parser = new ICAL.ComponentParser(options);
|
|
*
|
|
* parser.onevent() {
|
|
* //...
|
|
* }
|
|
*
|
|
* // ontimezone, etc...
|
|
*
|
|
* parser.oncomplete = function() {
|
|
*
|
|
* };
|
|
*
|
|
* parser.process(string | component);
|
|
*
|
|
*
|
|
* @param {Object} options component parser options.
|
|
*/
|
|
function ComponentParser(options) {
|
|
if (typeof(options) === 'undefined') {
|
|
options = {};
|
|
}
|
|
|
|
var key;
|
|
for (key in options) {
|
|
if (options.hasOwnProperty(key)) {
|
|
this[key] = options[key];
|
|
}
|
|
}
|
|
}
|
|
|
|
ComponentParser.prototype = {
|
|
|
|
/**
|
|
* When true parse events
|
|
*
|
|
* @type Boolean
|
|
*/
|
|
parseEvent: true,
|
|
|
|
/**
|
|
* when true parse timezones
|
|
*
|
|
* @type Boolean
|
|
*/
|
|
parseTimezone: true,
|
|
|
|
|
|
/* SAX like events here for reference */
|
|
|
|
/**
|
|
* Fired when parsing is complete
|
|
*/
|
|
oncomplete: function() {},
|
|
|
|
/**
|
|
* Fired if an error occurs during parsing.
|
|
*
|
|
* @param {Error} err details of error.
|
|
*/
|
|
onerror: function(err) {},
|
|
|
|
/**
|
|
* Fired when a top level component (vtimezone) is found
|
|
*
|
|
* @param {ICAL.Timezone} timezone object.
|
|
*/
|
|
ontimezone: function(component) {},
|
|
|
|
/*
|
|
* Fired when a top level component (VEVENT) is found.
|
|
* @param {ICAL.Event} component top level component.
|
|
*/
|
|
onevent: function(component) {},
|
|
|
|
/**
|
|
* Process a string or parse ical object.
|
|
* This function itself will return nothing but
|
|
* will start the parsing process.
|
|
*
|
|
* Events must be registered prior to calling this method.
|
|
*
|
|
* @param {String|Object} ical string or parsed ical object.
|
|
*/
|
|
process: function(ical) {
|
|
//TODO: this is sync now in the future we will have a incremental parser.
|
|
if (typeof(ical) === 'string') {
|
|
ical = ICAL.parse(ical)[1];
|
|
}
|
|
|
|
if (!(ical instanceof ICAL.Component)) {
|
|
ical = new ICAL.Component(ical);
|
|
}
|
|
|
|
var components = ical.getAllSubcomponents();
|
|
var i = 0;
|
|
var len = components.length;
|
|
var component;
|
|
|
|
for (; i < len; i++) {
|
|
component = components[i];
|
|
|
|
switch (component.name) {
|
|
case 'vtimezone':
|
|
if (this.parseTimezone) {
|
|
var tzid = component.getFirstPropertyValue('tzid');
|
|
if (tzid) {
|
|
this.ontimezone(new ICAL.Timezone({
|
|
tzid: tzid,
|
|
component: component
|
|
}));
|
|
}
|
|
}
|
|
break;
|
|
case 'vevent':
|
|
if (this.parseEvent) {
|
|
this.onevent(new ICAL.Event(component));
|
|
}
|
|
break;
|
|
default:
|
|
continue;
|
|
}
|
|
}
|
|
|
|
//XXX: ideally we should do a "nextTick" here
|
|
// so in all cases this is actually async.
|
|
this.oncomplete();
|
|
}
|
|
};
|
|
|
|
return ComponentParser;
|
|
|
|
}());
|