From 0cac01e4243b112f3c0820c89d9de2bf38005c79 Mon Sep 17 00:00:00 2001 From: James Lal Date: Wed, 19 Sep 2012 11:11:16 -0700 Subject: decouple ical-js will resolve #5 --- test/caldav/ical_test.js | 32 - test/caldav/sax/calendar_data_handler_test.js | 66 + test/caldav/sax/dav_response_test.js | 40 +- test/support/ical.js | 4613 +++++++++++++++++++++++++ 4 files changed, 4710 insertions(+), 41 deletions(-) delete mode 100644 test/caldav/ical_test.js create mode 100644 test/caldav/sax/calendar_data_handler_test.js create mode 100644 test/support/ical.js (limited to 'test') diff --git a/test/caldav/ical_test.js b/test/caldav/ical_test.js deleted file mode 100644 index 561bad3..0000000 --- a/test/caldav/ical_test.js +++ /dev/null @@ -1,32 +0,0 @@ -testSupport.lib('../ical'); - -suite('caldav/ics', function() { - - var ical; - var samples = {}; - - suiteSetup(function(done) { - testSupport.loadSample('ical/event.ics', function(err, data) { - samples.event = data; - done(); - }); - }); - - test('intiailizer', function() { - assert.ok(ICAL); - }); - - suite('VEVENT', function() { - var result; - - setup(function() { - result = ICAL.parse(samples.event); - }); - - test('parse', function() { - assert.equal(result.name, 'VCALENDAR'); - }); - - }); - -}); diff --git a/test/caldav/sax/calendar_data_handler_test.js b/test/caldav/sax/calendar_data_handler_test.js new file mode 100644 index 0000000..867cbe3 --- /dev/null +++ b/test/caldav/sax/calendar_data_handler_test.js @@ -0,0 +1,66 @@ +testSupport.lib('responder'); +testSupport.lib('sax'); +testSupport.lib('sax/base'); +testSupport.lib('sax/calendar_data_handler'); + +suite('caldav/sax/calendar_data_handler', function() { + + + var Parse; + var Base; + var Handler; + + suiteSetup(function() { + Parse = Caldav.require('sax'); + Base = Caldav.require('sax/base'); + Handler = Caldav.require('sax/calendar_data_handler'); + }); + + var subject; + var proxy; + + function callProxy() { + var args = Array.prototype.slice.call(arguments); + var method = args.shift(); + return subject[method].apply(proxy, args); + } + + setup(function() { + subject = Handler; + proxy = { + current: {}, + currentTag: [0], + handler: { tagField: 0 } + }; + }); + + suite('ontext', function() { + var originalHandler; + + suiteSetup(function() { + originalHandler = Handler.parseICAL; + }); + + teardown(function() { + Handler.parseICAL = originalHandler; + }); + + test('without handler', function() { + callProxy('ontext', 'foo'); + assert.equal(proxy.current[0], 'foo'); + }); + + test('with handler', function() { + var calledWith; + Handler.parseICAL = function() { + calledWith = arguments; + return 'hit'; + } + + callProxy('ontext', 'baz'); + assert.deepEqual(calledWith, ['baz']); + assert.equal(proxy.current[0], 'hit'); + }); + }); +}); + diff --git a/test/caldav/sax/dav_response_test.js b/test/caldav/sax/dav_response_test.js index b8f4716..926f975 100644 --- a/test/caldav/sax/dav_response_test.js +++ b/test/caldav/sax/dav_response_test.js @@ -1,20 +1,18 @@ +testSupport.helper('ical'); testSupport.lib('responder'); testSupport.lib('sax'); testSupport.lib('sax/base'); -testSupport.lib('ical'); +testSupport.lib('sax/calendar_data_handler'); testSupport.lib('sax/dav_response'); -testSupport.lib('../ical'); suite('caldav/sax/dav_response', function() { - var data, - subject, - parser, - Parse, - Response, - Base, - handler; + var Parse; + var Response; + var Base; + var CalendarDataHandler; + var originalHandler; suiteSetup(function() { Parse = Caldav.require('sax'); @@ -22,11 +20,35 @@ suite('caldav/sax/dav_response', function() { Response = Caldav.require('sax/dav_response'); }); + var subject; + var data; + var handler; + var parser; + setup(function() { //we omit the option to pass base parser //because we are the base parser subject = new Parse(); subject.registerHandler('DAV:/response', Response); + + // HACK to get CalendarDataHandler + var handleNS = 'urn:ietf:params:xml:ns:caldav/calendar-data'; + + CalendarDataHandler = subject.handles['DAV:/response']; + CalendarDataHandler = CalendarDataHandler.handles['DAV:/propstat']; + CalendarDataHandler = CalendarDataHandler.handles[handleNS]; + + if (!originalHandler) { + originalHandler = CalendarDataHandler.parseICAL; + } + + // XXX: this may change later if ICAL is no longer + // exposed directly. + CalendarDataHandler.parseICAL = ICAL.parse; + }); + + teardown(function() { + CalendarDataHandler.parseICAL = originalHandler; }); suite('calendar-query', function() { diff --git a/test/support/ical.js b/test/support/ical.js new file mode 100644 index 0000000..5012ca0 --- /dev/null +++ b/test/support/ical.js @@ -0,0 +1,4613 @@ +/* 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') + (typeof(window) !== 'undefined') ? this.ICAL = {} : ICAL = {}; + +/** + * 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: [] + }; + }, + + 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) { + if (aSrc.hasOwnProperty(name)) { + dump("Cloning " + name + "\n"); + 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) { + return ("00" + data).substr(-2); + }, + + trunc: function trunc(number) { + return (number < 0 ? Math.ceil(number) : Math.floor(number)); + } +}; +(typeof(ICAL) === 'undefined')? ICAL = {} : ''; + +(function() { + ICAL.serializer = { + serializeToIcal: function(obj, name, isParam) { + if (obj && obj.icalclass) { + return obj.toString(); + } + + var str = ""; + + if (obj.type == "COMPONENT") { + str = "BEGIN:" + obj.name + ICAL.newLineChar; + for (var subkey in obj.value) { + str += this.serializeToIcal(obj.value[subkey]) + ICAL.newLineChar; + } + str += "END:" + obj.name; + } else { + str += ICAL.icalparser.stringifyProperty(obj); + } + return str; + } + }; +}()); +/* 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 */ + +// TODO validate known parameters +// TODO make sure all known types don't contain junk +// TODO tests for parsers +// TODO SAX type parser +// TODO structure data in components +// TODO enforce uppercase when parsing +// TODO optionally preserve value types that are default but explicitly set +// TODO floating timezone +(typeof(ICAL) === 'undefined')? ICAL = {} : ''; +(function() { + /* NOTE: I'm not sure this is the latest syntax... + + { + X-WR-CALNAME: "test", + components: { + VTIMEZONE: { ... }, + VEVENT: { + "uuid1": { + UID: "uuid1", + ... + components: { + VALARM: [ + ... + ] + } + } + }, + VTODO: { ... } + } + } + */ + + // Exports + + function ParserError(aState, aMessage) { + this.mState = aState; + this.name = "ParserError"; + if (aState) { + var lineNrData = ("lineNr" in aState ? aState.lineNr + ":" : "") + + ("character" in aState && !isNaN(aState.character) ? + aState.character + ":" : + ""); + + var message = lineNrData + aMessage; + if ("buffer" in aState) { + if (aState.buffer) { + message += " before '" + aState.buffer + "'"; + } else { + message += " at end of line"; + } + } + if ("line" in aState) { + message += " in '" + aState.line + "'"; + } + this.message = message; + } else { + this.message = aMessage; + } + + // create stack + try { + throw new Error(); + } catch (e) { + var split = e.stack.split('\n'); + split.shift(); + this.stack = split.join('\n'); + } + } + + ParserError.prototype = { + __proto__: Error.prototype, + constructor: ParserError + }; + + var parser = {}; + ICAL.icalparser = parser; + + parser.lexContentLine = function lexContentLine(aState) { + // contentline = name *(";" param ) ":" value CRLF + // The corresponding json object will be: + // { name: "name", parameters: { key: "value" }, value: "value" } + var lineData = {}; + + // Parse the name + lineData.name = parser.lexName(aState); + + // Read Paramaters, if there are any. + if (aState.buffer.substr(0, 1) == ";") { + lineData.parameters = {}; + while (aState.buffer.substr(0, 1) == ";") { + aState.buffer = aState.buffer.substr(1); + var param = parser.lexParam(aState); + lineData.parameters[param.name] = param.value; + } + } + + // Read the value + parser.expectRE(aState, /^:/, "Expected ':'"); + lineData.value = parser.lexValue(aState); + parser.expectEnd(aState, "Junk at End of Line"); + return lineData; + }; + + parser.lexName = function lexName(aState) { + function parseIanaToken(aState) { + var match = parser.expectRE(aState, /^([A-Za-z0-9-]+)/, + "Expected IANA Token"); + return match[1]; + } + + function parseXName(aState) { + var error = "Expected XName"; + var value = "X-"; + var match = parser.expectRE(aState, /^X-/, error); + + // Vendor ID + if ((match = parser.expectOptionalRE(aState, /^([A-Za-z0-9]+-)/, error))) { + value += match[1]; + } + + // Remaining part + match = parser.expectRE(aState, /^([A-Za-z0-9-]+)/, error); + value += match[1]; + + return value; + } + return parser.parseAlternative(aState, parseXName, parseIanaToken); + }; + + parser.lexValue = function lexValue(aState) { + // VALUE-CHAR = WSP / %x21-7E / NON-US-ASCII + // ; Any textual character + + if (aState.buffer.length === 0) { + return aState.buffer; + } + + // TODO the unicode range might be wrong! + var match = parser.expectRE(aState, + /* WSP|%x21-7E|NON-US-ASCII */ + /^([ \t\x21-\x7E\u00C2-\uF400]+)/, + "Invalid Character in value"); + + return match[1]; + }; + + parser.lexParam = function lexParam(aState) { + // read param name + var name = parser.lexName(aState); + parser.expectRE(aState, /^=/, "Expected '='"); + + // read param value + var values = parser.parseList(aState, parser.lexParamValue, ","); + return { + name: name, + value: (values.length == 1 ? values[0] : values) + }; + }; + + parser.lexParamValue = function lexParamValue(aState) { + // CONTROL = %x00-08 / %x0A-1F / %x7F + // ; All the controls except HTAB + function parseQuotedString(aState) { + parser.expectRE(aState, /^"/, "Expecting Quote Character"); + // QSAFE-CHAR = WSP / %x21 / %x23-7E / NON-US-ASCII + // ; Any character except CONTROL and DQUOTE + + var match = parser.expectRE(aState, /^([^"\x00-\x08\x0A-\x1F\x7F]*)/, + "Invalid Param Value"); + parser.expectRE(aState, /^"/, "Expecting Quote Character"); + return match[1]; + } + + function lexParamText(aState) { + // SAFE-CHAR = WSP / %x21 / %x23-2B / %x2D-39 / %x3C-7E / NON-US-ASCII + // ; Any character except CONTROL, DQUOTE, ";", ":", "," + var match = parser.expectRE(aState, /^([^";:,\x00-\x08\x0A-\x1F\x7F]*)/, + "Invalid Param Value"); + return match[1]; + } + + return parser.parseAlternative(aState, parseQuotedString, lexParamText); + }; + + parser.parseContentLine = function parseContentLine(aState, aLineData) { + + switch (aLineData.name) { + case "BEGIN": + var newdata = ICAL.helpers.initComponentData(aLineData.value); + if (aState.currentData) { + // If there is already data (i.e this is not the top level + // component), then push the new data to its values and + // stack the parent data. + aState.currentData.value.push(newdata); + aState.parentData.push(aState.currentData); + } + + aState.currentData = newdata; // set the new data array + break; + case "END": + if (aState.currentData.name != aLineData.value) { + throw new ParserError(aState, "Unexpected END:" + aLineData.value + + ", expected END:" + aState.currentData.name); + } + if (aState.parentData.length) { + aState.currentData = aState.parentData.pop(); + } + break; + default: + ICAL.helpers.dumpn("parse " + aLineData.toString()); + parser.detectParameterType(aLineData); + parser.detectValueType(aLineData); + ICAL.helpers.dumpn("parse " + aLineData.toString()); + aState.currentData.value.push(aLineData); + break; + } + }, + + parser.detectParameterType = function detectParameterType(aLineData) { + for (var name in aLineData.parameters) { + var paramType = "TEXT"; + + if (name in ICAL.design.param && "valueType" in ICAL.design.param[name]) { + paramType = ICAL.design.param[name].valueType; + } + var paramData = { + value: aLineData.parameters[name], + type: paramType + }; + + aLineData.parameters[name] = paramData; + } + }; + + parser.detectValueType = function detectValueType(aLineData) { + var valueType = "TEXT"; + var defaultType = null; + if (aLineData.name in ICAL.design.property && + "defaultType" in ICAL.design.property[aLineData.name]) { + valueType = ICAL.design.property[aLineData.name].defaultType; + } + + if ("parameters" in aLineData && "VALUE" in aLineData.parameters) { + ICAL.helpers.dumpn("VAAAA: " + aLineData.parameters.VALUE.toString()); + valueType = aLineData.parameters.VALUE.value.toUpperCase(); + } + + if (!(valueType in ICAL.design.value)) { + throw new ParserError(aLineData, "Invalid VALUE Type '" + valueType); + } + + aLineData.type = valueType; + + // It could be a multi-value value, we have to take that apart first + function unwrapMultiValue(x, separator) { + var values = []; + + function replacer(s, a) { + values.push(a); + return ""; + } + var re = new RegExp("(.*?[^\\\\])" + separator, "g"); + values.push(x.replace(re, replacer)); + return values; + } + + if (aLineData.name in ICAL.design.property) { + if (ICAL.design.property[aLineData.name].multiValue) { + aLineData.value = unwrapMultiValue(aLineData.value, ","); + } else if (ICAL.design.property[aLineData.name].structuredValue) { + aLineData.value = unwrapMultiValue(aLineData.value, ";"); + } else { + aLineData.value = [aLineData.value]; + } + } else { + aLineData.value = [aLineData.value]; + } + + if ("unescape" in ICAL.design.value[valueType]) { + var unescaper = ICAL.design.value[valueType].unescape; + for (var idx in aLineData.value) { + aLineData.value[idx] = unescaper(aLineData.value[idx], aLineData.name); + } + } + + return aLineData; + } + + parser.validateValue = function validateValue(aLineData, aValueType, + aValue, aCheckParams) { + var propertyData = ICAL.design.property[aLineData.name]; + var valueData = ICAL.design.value[aValueType]; + + // TODO either make validators just consume the value, then check for end + // here (possibly requires returning remainder or renaming buffer<->value + // in the states) validators don't really need the whole linedata + + if (!aValue.match) { + ICAL.helpers.dumpn("MAAA: " + aValue + " ? " + aValue.toString()); + } + + if (valueData.matches) { + // Test against regex + if (!aValue.match(valueData.matches)) { + throw new ParserError(aLineData, "Value '" + aValue + "' for " + + aLineData.name + " is not " + aValueType); + } + } else if ("validate" in valueData) { + // Validator throws an error itself if needed + var objData = valueData.validate(aValue); + + // Merge in extra value data, if it exists + ICAL.helpers.mixin(aLineData, objData); + } else if ("values" in valueData) { + // Fixed list of values + if (valueData.values.indexOf(aValue) < 0) { + throw new ParserError(aLineData, "Value for " + aLineData.name + + " is not a " + aValueType); + } + } + + if (aCheckParams && "requireParam" in valueData) { + var reqParam = valueData.requireParam; + for (var param in reqParam) { + if (!("parameters" in aLineData) || + !(param in aLineData.parameters) || + aLineData.parameters[param].value != reqParam[param]) { + + throw new ParserError(aLineData, "Value requires " + param + "=" + + valueData.requireParam[param]); + } + } + } + + return aLineData; + }; + + parser.parseValue = function parseValue(aStr, aType) { + var lineData = { + value: [aStr] + }; + return parser.validateValue(lineData, aType, aStr, false); + }; + + parser.decorateValue = function decorateValue(aType, aValue) { + if (aType in ICAL.design.value && "decorate" in ICAL.design.value[aType]) { + return ICAL.design.value[aType].decorate(aValue); + } else { + return ICAL.design.value.TEXT.decorate(aValue); + } + }; + + parser.stringifyProperty = function stringifyProperty(aLineData) { + ICAL.helpers.dumpn("Stringify: " + aLineData.toString()); + var str = aLineData.name; + if (aLineData.parameters) { + for (var key in aLineData.parameters) { + str += ";" + key + "=" + aLineData.parameters[key].value; + } + } + + str += ":" + parser.stringifyValue(aLineData); + + return ICAL.helpers.foldline(str); + }; + + parser.stringifyValue = function stringifyValue(aLineData) { + function arrayStringMap(arr, func) { + var newArr = []; + for (var idx in arr) { + newArr[idx] = func(arr[idx].toString()); + } + return newArr; + } + + if (aLineData) { + var values = aLineData.value; + if (aLineData.type in ICAL.design.value && + "escape" in ICAL.design.value[aLineData.type]) { + var escaper = ICAL.design.value[aLineData.type].escape; + values = arrayStringMap(values, escaper); + } + + var separator = ","; + if (aLineData.name in ICAL.design.property && + ICAL.design.property[aLineData.name].structuredValue) { + separator = ";"; + } + + return values.join(separator); + } else { + return null; + } + }; + + parser.parseDateOrDateTime = function parseDateOrDateTime(aState) { + var data = parser.parseDate(aState); + + if (parser.expectOptionalRE(aState, /^T/)) { + // This has a time component, parse it + var time = parser.parseTime(aState); + + if (parser.expectOptionalRE(aState, /^Z/)) { + data.timezone = "Z"; + } + ICAL.helpers.mixin(data, time); + } + return data; + }; + + parser.parseDateTime = function parseDateTime(aState) { + var data = parser.parseDate(aState); + parser.expectRE(aState, /^T/, "Expected 'T'"); + + var time = parser.parseTime(aState); + + if (parser.expectOptionalRE(aState, /^Z/)) { + data.timezone = "Z"; + } + + ICAL.helpers.mixin(data, time); + return data; + }; + + parser.parseDate = function parseDate(aState) { + var dateRE = /^((\d{4})(0[1-9]|1[0-2])(0[1-9]|[12][0-9]|3[01]))/; + var match = parser.expectRE(aState, dateRE, "Expected YYYYMMDD Date"); + return { + year: parseInt(match[2], 10), + month: parseInt(match[3], 10), + day: parseInt(match[4], 10) + }; + // TODO timezone? + }; + + parser.parseTime = function parseTime(aState) { + var timeRE = /^(([01][0-9]|2[0-3])([0-5][0-9])([0-5][0-9]|60))/; + var match = parser.expectRE(aState, timeRE, "Expected HHMMSS Time"); + return { + hour: parseInt(match[2], 10), + minute: parseInt(match[3], 10), + second: parseInt(match[4], 10) + }; + }; + + parser.parseDuration = function parseDuration(aState) { + var error = "Expected Duration Value"; + + function parseDurSecond(aState) { + var secMatch = parser.expectRE(aState, /^((\d+)S)/, "Expected Seconds"); + return { + seconds: parseInt(secMatch[2], 10) + }; + } + + function parseDurMinute(aState) { + var data = {}; + var minutes = parser.expectRE(aState, /^((\d+)M)/, "Expected Minutes"); + try { + data = parseDurSecond(aState); + } catch (e) { + // seconds are optional, its ok + if (!(e instanceof ParserError)) { + throw e; + } + } + data.minutes = parseInt(minutes[2], 10); + return data; + } + + function parseDurHour(aState) { + var data = {}; + var hours = parser.expectRE(aState, /^((\d+)H)/, "Expected Hours"); + try { + data = parseDurMinute(aState); + } catch (e) { + // seconds are optional, its ok + if (!(e instanceof ParserError)) { + throw e; + } + } + + data.hours = parseInt(hours[2], 10); + return data; + } + + function parseDurWeek(aState) { + return { + weeks: parser.expectRE(aState, /^((\d+)W)/, "Expected Weeks")[2] + }; + } + + function parseDurTime(aState) { + parser.expectRE(aState, /^T/, "Expected Time Value"); + return parser.parseAlternative(aState, parseDurHour, + parseDurMinute, parseDurSecond); + } + + function parseDurDate(aState) { + var days = parser.expectRE(aState, /^((\d+)D)/, "Expected Days"); + var data; + + try { + data = parseDurTime(aState); + } catch (e) { + // Its ok if this fails + if (!(e instanceof ParserError)) { + throw e; + } + } + + if (data) { + data.days = days[2]; + } else { + data = { + days: parseInt(days[2], 10) + }; + } + return data; + } + + var factor = parser.expectRE(aState, /^([+-]?P)/, error); + + var durData = parser.parseAlternative(aState, parseDurDate, + parseDurTime, parseDurWeek); + parser.expectEnd(aState, "Junk at end of DURATION value"); + + durData.factor = (factor[1] == "-P" ? -1 : 1); + return durData; + }; + + parser.parsePeriod = function parsePeriod(aState) { + var dtime = parser.parseDateTime(aState); + parser.expectRE(aState, /\//, "Expected '/'"); + + var dtdur = parser.parseAlternative(aState, parser.parseDateTime, + parser.parseDuration); + var data = { + start: dtime + }; + if ("factor" in dtdur) { + data.duration = dtdur; + } else { + data.end = dtdur; + } + return data; + }, + + parser.parseRecur = function parseRecur(aState) { + // TODO this function is quite cludgy, maybe it should be done differently + function parseFreq(aState) { + parser.expectRE(aState, /^FREQ=/, "Expected Frequency"); + var ruleRE = /^(SECONDLY|MINUTELY|HOURLY|DAILY|WEEKLY|MONTHLY|YEARLY)/; + var match = parser.expectRE(aState, ruleRE, "Exepected Frequency Value"); + return { + "FREQ": match[1] + }; + } + + function parseUntil(aState) { + parser.expectRE(aState, /^UNTIL=/, "Expected Frequency"); + var untilDate = parser.parseDateOrDateTime(aState); + return { + "UNTIL": untilDate + }; + } + + function parseCount(aState) { + parser.expectRE(aState, /^COUNT=/, "Expected Count"); + var match = parser.expectRE(aState, /^(\d+)/, "Expected Digit(s)"); + return { + "COUNT": parseInt(match[1], 10) + }; + } + + function parseInterval(aState) { + parser.expectRE(aState, /^INTERVAL=/, "Expected Interval"); + var match = parser.expectRE(aState, /^(\d+)/, "Expected Digit(s)"); + return { + "INTERVAL": parseInt(match[1], 10) + }; + } + + function parseBySecond(aState) { + function parseSecond(aState) { + var secondRE = /^(60|[1-5][0-9]|[0-9])/; + var value = parser.expectRE(aState, secondRE, "Expected Second")[1]; + return parseInt(value, 10); + } + parser.expectRE(aState, /^BYSECOND=/, "Expected BYSECOND"); + var seconds = parser.parseList(aState, parseSecond, ","); + return { + "BYSECOND": seconds + }; + } + + function parseByMinute(aState) { + function parseMinute(aState) { + var minuteRE = /^([1-5][0-9]|[0-9])/; + var value = parser.expectRE(aState, minuteRE, "Expected Minute")[1]; + return parseInt(value, 10); + } + parser.expectRE(aState, /^BYMINUTE=/, "Expected BYMINUTE"); + var minutes = parser.parseList(aState, parseMinute, ","); + return { + "BYMINUTE": minutes + }; + } + + function parseByHour(aState) { + function parseHour(aState) { + var hourRE = /^(2[0-3]|1[0-9]|[0-9])/; + var value = parser.expectRE(aState, hourRE, "Expected Hour")[1]; + return parseInt(value, 10); + } + parser.expectRE(aState, /^BYHOUR=/, "Expected BYHOUR"); + var hours = parser.parseList(aState, parseHour, ","); + return { + "BYHOUR": hours + }; + } + + function parseByDay(aState) { + function parseWkDayNum(aState) { + var value = ""; + var match = parser.expectOptionalRE(aState, /^([+-])/); + if (match) { + value += match[1]; + } + + match = parser.expectOptionalRE(aState, /^(5[0-3]|[1-4][0-9]|[1-9])/); + if (match) { + value += match[1]; + } + + var wkDayRE = /^(SU|MO|TU|WE|TH|FR|SA)/; + match = parser.expectRE(aState, wkDayRE, "Expected Week Ordinals"); + value += match[1]; + return value; + } + parser.expectRE(aState, /^BYDAY=/, "Expected BYDAY Rule"); + var wkdays = parser.parseList(aState, parseWkDayNum, ","); + return { + "BYDAY": wkdays + }; + } + + function parseByMonthDay(aState) { + function parseMoDayNum(aState) { + var value = ""; + var match = parser.expectOptionalRE(aState, /^([+-])/); + if (match) { + value += match[1]; + } + + match = parser.expectRE(aState, /^(3[01]|[12][0-9]|[1-9])/); + value += match[1]; + return parseInt(value, 10); + } + parser.expectRE(aState, /^BYMONTHDAY=/, "Expected BYMONTHDAY Rule"); + var modays = parser.parseList(aState, parseMoDayNum, ","); + return { + "BYMONTHDAY": modays + }; + } + + function parseByYearDay(aState) { + function parseYearDayNum(aState) { + var value = ""; + var match = parser.expectOptionalRE(aState, /^([+-])/); + if (match) { + value += match[1]; + } + + var yrDayRE = /^(36[0-6]|3[0-5][0-9]|[12][0-9][0-9]|[1-9][0-9]|[1-9])/; + match = parser.expectRE(aState, yrDayRE); + value += match[1]; + return parseInt(value, 10); + } + parser.expectRE(aState, /^BYYEARDAY=/, "Expected BYYEARDAY Rule"); + var yrdays = parser.parseList(aState, parseYearDayNum, ","); + return { + "BYYEARDAY": yrdays + }; + } + + function parseByWeekNo(aState) { + function parseWeekNum(aState) { + var value = ""; + var match = parser.expectOptionalRE(aState, /^([+-])/); + if (match) { + value += match[1]; + } + + match = parser.expectRE(aState, /^(5[0-3]|[1-4][0-9]|[1-9])/); + value += match[1]; + return parseInt(value, 10); + } + parser.expectRE(aState, /^BYWEEKNO=/, "Expected BYWEEKNO Rule"); + var weeknos = parser.parseList(aState, parseWeekNum, ","); + return { + "BYWEEKNO": weeknos + }; + } + + function parseByMonth(aState) { + function parseMonthNum(aState) { + var moNumRE = /^(1[012]|[1-9])/; + var match = parser.expectRE(aState, moNumRE, "Expected Month number"); + return parseInt(match[1], 10); + } + parser.expectRE(aState, /^BYMONTH=/, "Expected BYMONTH Rule"); + var monums = parser.parseList(aState, parseMonthNum, ","); + return { + "BYMONTH": monums + }; + } + + function parseBySetPos(aState) { + function parseSpList(aState) { + var spRE = /^(36[0-6]|3[0-5][0-9]|[12][0-9][0-9]|[1-9][0-9]|[1-9])/; + var value = parser.expectRE(aState, spRE)[1]; + + return parseInt(value, 10); + } + parser.expectRE(aState, /^BYSETPOS=/, "Expected BYSETPOS Rule"); + var spnums = parser.parseList(aState, parseSpList, ","); + return { + "BYSETPOS": spnums + }; + } + + function parseWkst(aState) { + parser.expectRE(aState, /^WKST=/, "Expected WKST"); + var wkstRE = /^(SU|MO|TU|WE|TH|FR|SA)/; + var match = parser.expectRE(aState, wkstRE, "Expected Weekday Name"); + return { + "WKST": match[1] + }; + } + + function parseRulePart(aState) { + return parser.parseAlternative(aState, + parseFreq, parseUntil, parseCount, parseInterval, + parseBySecond, parseByMinute, parseByHour, parseByDay, + parseByMonthDay, parseByYearDay, parseByWeekNo, + parseByMonth, parseBySetPos, parseWkst); + } + + // One or more rule parts + var value = parser.parseList(aState, parseRulePart, ";"); + var data = {}; + for (var key in value) { + ICAL.helpers.mixin(data, value[key]); + } + + // Make sure there's no junk at the end + parser.expectEnd(aState, "Junk at end of RECUR value"); + return data; + }; + + parser.parseUtcOffset = function parseUtcOffset(aState) { + var utcRE = /^(([+-])([01][0-9]|2[0-3])([0-5][0-9])([0-5][0-9])?)$/; + var match = parser.expectRE(aState, utcRE, "Expected valid utc offset"); + return { + factor: (match[2] == "-" ? -1 : 1), + hours: parseInt(match[3], 10), + minutes: parseInt(match[4], 10) + }; + }; + + parser.parseAlternative = function parseAlternative(aState /*, parserFunc, ... */) { + var tokens = null; + var args = Array.prototype.slice.call(arguments); + var parser; + args.shift(); + var errors = []; + + while (!tokens && (parser = args.shift())) { + try { + tokens = parser(aState); + } catch (e) { + if (e instanceof ParserError) { + errors.push(e); + tokens = null; + } else { + throw e; + } + } + } + + if (!tokens) { + var message = errors.join("\nOR ") || "No Tokens found"; + throw new ParserError(aState, message); + } + + return tokens; + }, + + parser.parseList = function parseList(aState, aElementFunc, aSeparator) { + var listvals = []; + + listvals.push(aElementFunc(aState)); + var re = new RegExp("^" + aSeparator + ""); + while (parser.expectOptionalRE(aState, re)) { + listvals.push(aElementFunc(aState)); + } + return listvals; + }; + + parser.expectOptionalRE = function expectOptionalRE(aState, aRegex) { + var match = aState.buffer.match(aRegex); + if (match) { + var count = ("1" in match ? match[1].length : match[0].length); + aState.buffer = aState.buffer.substr(count); + aState.character += count; + } + return match; + }; + + parser.expectRE = function expectRE(aState, aRegex, aErrorMessage) { + var match = parser.expectOptionalRE(aState, aRegex); + if (!match) { + throw new ParserError(aState, aErrorMessage); + } + return match; + }; + + parser.expectEnd = function expectEnd(aState, aErrorMessage) { + if (aState.buffer.length > 0) { + throw new ParserError(aState, aErrorMessage); + } + } + + /* Possible shortening: + - pro: retains order + - con: datatypes not obvious + - pro: not so many objects created + + { + "begin:vcalendar": [ + { + prodid: "-//Example Inc.//Example Client//EN", + version: "2.0" + "begin:vtimezone": [ + { + "last-modified": [{ + type: "date-time", + value: "2004-01-10T03:28:45Z" + }], + tzid: "US/Eastern" + "begin:daylight": [ + { + dtstart: { + type: "date-time", + value: "2000-04-04T02:00:00" + } + rrule: { + type: "recur", + value: { + freq: "YEARLY", + byday: ["1SU"], + bymonth: ["4"], + } + } + } + ] + } + ], + "begin:vevent": [ + { + category: [{ + type: "text" + // have icalcomponent take apart the multivalues + value: "multi1,multi2,multi3" + },{ + type "text" + value: "otherprop1" + }] + } + ] + } + ] + } + */ +})(); +/* 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 = {} : ''; + +/** + * Design data used by the parser to decide if data is semantically correct + */ +ICAL.design = { + 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: true + }, + "DELEGATED-TO": { + valueType: "CAL-ADDRESS", + multiValue: true + }, + // "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: true + }, + "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: ["THISANDFUTURE"] + }, + "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": { + 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": { + matches: /^([A-Za-z0-9+\/]{4})*([A-Za-z0-9+\/]{2}==|[A-Za-z0-9+\/]{3}=)?$/, + requireParam: { + "ENCODING": "BASE64" + }, + decorate: function(aString) { + return ICAL.icalbinary.fromString(aString); + } + }, + "BOOLEAN": { + values: ["TRUE", "FALSE"], + decorate: function(aValue) { + return ICAL.icalvalue.fromString(aValue, "BOOLEAN"); + } + }, + "CAL-ADDRESS": { + // needs to be an uri + }, + "DATE": { + validate: function(aValue) { + var state = { + buffer: aValue + }; + var data = ICAL.icalparser.parseDate(state); + ICAL.icalparser.expectEnd(state, "Junk at end of DATE value"); + return data; + }, + decorate: function(aValue) { + return ICAL.icaltime.fromString(aValue); + } + }, + "DATE-TIME": { + validate: function(aValue) { + var state = { + buffer: aValue + }; + var data = ICAL.icalparser.parseDateTime(state); + ICAL.icalparser.expectEnd(state, "Junk at end of DATE-TIME value"); + return data; + }, + + decorate: function(aValue) { + return ICAL.icaltime.fromString(aValue); + } + }, + "DURATION": { + validate: function(aValue) { + var state = { + buffer: aValue + }; + var data = ICAL.icalparser.parseDuration(state); + ICAL.icalparser.expectEnd(state, "Junk at end of DURATION value"); + return data; + }, + decorate: function(aValue) { + return ICAL.icalduration.fromString(aValue); + } + }, + "FLOAT": { + matches: /^[+-]?\d+\.\d+$/, + decorate: function(aValue) { + return ICAL.icalvalue.fromString(aValue, "FLOAT"); + } + }, + "INTEGER": { + matches: /^[+-]?\d+$/, + decorate: function(aValue) { + return ICAL.icalvalue.fromString(aValue, "INTEGER"); + } + }, + "PERIOD": { + validate: function(aValue) { + var state = { + buffer: aValue + }; + var data = ICAL.icalparser.parsePeriod(state); + ICAL.icalparser.expectEnd(state, "Junk at end of PERIOD value"); + return data; + }, + + decorate: function(aValue) { + return ICAL.icalperiod.fromString(aValue); + } + }, + "RECUR": { + validate: function(aValue) { + var state = { + buffer: aValue + }; + var data = ICAL.icalparser.parseRecur(state); + ICAL.icalparser.expectEnd(state, "Junk at end of RECUR value"); + return data; + }, + + decorate: function decorate(aValue) { + return ICAL.icalrecur.fromString(aValue); + } + }, + + "TEXT": { + matches: /.*/, + decorate: function(aValue) { + return ICAL.icalvalue.fromString(aValue, "TEXT"); + }, + unescape: function(aValue, aName) { + return aValue.replace(/\\\\|\\;|\\,|\\[Nn]/g, function(str) { + switch (str) { + case "\\\\": + return "\\"; + case "\\;": + return ";"; + case "\\,": + return ","; + case "\\n": + case "\\N": + return "\n"; + default: + return str; + } + }); + }, + + escape: 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": { + validate: function(aValue) { + var state = { + buffer: aValue + }; + var data = ICAL.icalparser.parseTime(state); + ICAL.icalparser.expectEnd(state, "Junk at end of TIME value"); + return data; + } + }, + + "URI": { + // TODO + /* ... */ + }, + + "UTC-OFFSET": { + validate: function(aValue) { + var state = { + buffer: aValue + }; + var data = ICAL.icalparser.parseUtcOffset(state); + ICAL.icalparser.expectEnd(state, "Junk at end of UTC-OFFSET value"); + return data; + }, + + decorate: function(aValue) { + return ICAL.icalutcoffset.fromString(aValue); + } + } + }, + + property: { + decorate: function decorate(aData, aParent) { + return new ICAL.icalproperty(aData, aParent); + }, + "ATTACH": { + defaultType: "URI" + }, + "ATTENDEE": { + defaultType: "CAL-ADDRESS" + }, + "CATEGORIES": { + defaultType: "TEXT", + multiValue: true + }, + "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"] + }, + "EXRULE": { + defaultType: "RECUR" + }, + "FREEBUSY": { + defaultType: "PERIOD", + multiValue: true + }, + "GEO": { + defaultType: "FLOAT", + structuredValue: true + }, + /* 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"] + }, + "RECURRENCE-ID": { + defaultType: "DATE-TIME", + allowedTypes: ["DATE-TIME", "DATE"] + }, + "RESOURCES": { + defaultType: "TEXT", + multiValue: true + }, + "REQUEST-STATUS": { + defaultType: "TEXT", + structuredValue: true + }, + "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.icalcomponent(aData, aParent); + }, + "VEVENT": {} + } +}; +/* 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.icalcomponent = function icalcomponent(data, parent) { + this.wrappedJSObject = this; + this.parent = parent; + this.fromData(data); + } + + ICAL.icalcomponent.prototype = { + + data: null, + name: "", + components: null, + properties: null, + + icalclass: "icalcomponent", + + clone: function clone() { + return new ICAL.icalcomponent(this.undecorate(), this.parent); + }, + + fromData: function fromData(data) { + if (!data) { + data = ICAL.helpers.initComponentData(null); + } + this.data = data; + this.data.value = this.data.value || []; + this.data.type = this.data.type || "COMPONENT"; + this.components = {}; + this.properties = {}; + + // Save the name directly on the object, as we want this accessed + // from the outside. + this.name = this.data.name; + delete this.data.name; + + var value = this.data.value; + + for (var key in value) { + var keyname = value[key].name; + if (value[key].type == "COMPONENT") { + value[key] = new ICAL.icalcomponent(value[key], this); + ICAL.helpers.ensureKeyExists(this.components, keyname, []); + this.components[keyname].push(value[key]); + } else { + value[key] = new ICAL.icalproperty(value[key], this); + ICAL.helpers.ensureKeyExists(this.properties, keyname, []); + this.properties[keyname].push(value[key]); + } + } + }, + + undecorate: function undecorate() { + var newdata = []; + for (var key in this.data.value) { + newdata.push(this.data.value[key].undecorate()); + } + return { + name: this.name, + type: "COMPONENT", + value: newdata + }; + }, + + getFirstSubcomponent: function getFirstSubcomponent(aType) { + var comp = null; + if (aType) { + var ucType = aType.toUpperCase(); + if (ucType in this.components && + this.components[ucType] && + this.components[ucType].length > 0) { + comp = this.components[ucType][0]; + } + } else { + for (var thiscomp in this.components) { + comp = this.components[thiscomp][0]; + break; + } + } + return comp; + }, + + getAllSubcomponents: function getAllSubcomponents(aType) { + var comps = []; + if (aType && aType != "ANY") { + var ucType = aType.toUpperCase(); + if (ucType in this.components) { + for (var compKey in this.components[ucType]) { + comps.push(this.components[ucType][compKey]); + } + } + } else { + for (var compName in this.components) { + for (var compKey in this.components[compName]) { + comps.push(this.components[compName][compKey]); + } + } + } + return comps; + }, + + addSubcomponent: function addSubcomponent(aComp, aCompName) { + var ucName, comp; + var comp; + if (aComp.icalclass == "icalcomponent") { + ucName = aComp.name; + comp = aComp.clone(); + comp.parent = this; + } else { + ucName = aCompName.toUpperCase(); + comp = new ICAL.icalcomponent(aComp, ucName, this); + } + + this.data.value.push(comp); + ICAL.helpers.ensureKeyExists(this.components, ucName, []); + this.components[ucName].push(comp); + }, + + removeSubcomponent: function removeSubComponent(aName) { + var ucName = aName.toUpperCase(); + for (var key in this.components[ucName]) { + var pos = this.data.value.indexOf(this.components[ucName][key]); + if (pos > -1) { + this.data.value.splice(pos, 1); + } + } + + delete this.components[ucName]; + }, + + hasProperty: function hasProperty(aName) { + var ucName = aName.toUpperCase(); + return (ucName in this.properties); + }, + + getFirstProperty: function getFirstProperty(aName) { + var prop = null; + if (aName) { + var ucName = aName.toUpperCase(); + if (ucName in this.properties && this.properties[ucName]) { + prop = this.properties[ucName][0]; + } + } else { + for (var p in this.properties) { + prop = this.properties[p]; + break; + } + } + return prop; + }, + + getFirstPropertyValue: function getFirstPropertyValue(aName) { + // TODO string value? + var prop = this.getFirstProperty(aName); + return (prop ? prop.getFirstValue() : null); + }, + + getAllProperties: function getAllProperties(aName) { + var props = []; + if (aName && aName != "ANY") { + var ucType = aName.toUpperCase(); + if (ucType in this.properties) { + props = this.properties[ucType].concat([]); + } + } else { + for (var propName in this.properties) { + props = props.concat(this.properties[propName]); + } + } + return props; + }, + + addPropertyWithValue: function addStringProperty(aName, aValue) { + var ucName = aName.toUpperCase(); + var lineData = ICAL.icalparser.detectValueType({ + name: ucName, + value: aValue + }); + + var prop = ICAL.icalproperty.fromData(lineData); + ICAL.helpers.dumpn("Adding property " + ucName + "=" + aValue); + return this.addProperty(prop); + }, + + addProperty: function addProperty(aProp) { + var prop = aProp; + if (aProp.parent) { + prop = aProp.clone(); + } + aProp.parent = this; + + ICAL.helpers.ensureKeyExists(this.properties, aProp.name, []); + this.properties[aProp.name].push(aProp); + ICAL.helpers.dumpn("DATA IS: " + this.data.toString()); + this.data.value.push(aProp); + ICAL.helpers.dumpn("Adding property " + aProp); + }, + + removeProperty: function removeProperty(aName) { + var ucName = aName.toUpperCase(); + for (var key in this.properties[ucName]) { + var pos = this.data.value.indexOf(this.properties[ucName][key]); + if (pos > -1) { + this.data.value.splice(pos, 1); + } + } + delete this.properties[ucName]; + }, + + clearAllProperties: function clearAllProperties() { + this.properties = {}; + for (var i = this.data.value.length - 1; i >= 0; i--) { + if (this.data.value[i].type != "COMPONENT") { + delete this.data.value[i]; + } + } + }, + + _valueToJSON: function(value) { + if (value && value.icaltype) { + return value.toString(); + } + + if (typeof(value) === 'object') { + return this._undecorateJSON(value); + } + + return value; + }, + + _undecorateJSON: function(object) { + if (object instanceof Array) { + var result = []; + var len = object.length; + + for (var i = 0; i < len; i++) { + result.push(this._valueToJSON(object[i])); + } + + } else { + var result = {}; + var key; + + for (key in object) { + if (object.hasOwnProperty(key)) { + result[key] = this._valueToJSON(object[key]); + } + } + } + + return result; + }, + + /** + * Exports the components values to a json friendly + * object. You can use JSON.stringify directly on + * components as a result. + */ + toJSON: function toJSON() { + return this._undecorateJSON(this.undecorate()); + }, + + toString: function toString() { + var str = ICAL.helpers.foldline("BEGIN:" + this.name) + ICAL.newLineChar; + for (var key in this.data.value) { + str += this.data.value[key].toString() + ICAL.newLineChar; + } + str += ICAL.helpers.foldline("END:" + this.name); + return str; + } + }; + + ICAL.icalcomponent.fromString = function icalcomponent_from_string(str) { + return ICAL.toJSON(str, true); + }; + + ICAL.icalcomponent.fromData = function icalcomponent_from_data(aData) { + return new ICAL.icalcomponent(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() { + ICAL.icalproperty = function icalproperty(data, parent) { + this.wrappedJSObject = this; + this.parent = parent; + this.fromData(data); + } + + ICAL.icalproperty.prototype = { + parent: null, + data: null, + name: null, + icalclass: "icalproperty", + + clone: function clone() { + return new ICAL.icalproperty(this.undecorate(), this.parent); + }, + + fromData: function fromData(aData) { + if (!aData.name) { + ICAL.helpers.dumpn("Missing name: " + aData.toString()); + } + this.name = aData.name; + this.data = aData; + this.setValues(this.data.value, this.data.type); + delete this.data.name; + }, + + undecorate: function() { + var values = []; + for (var key in this.data.value) { + var val = this.data.value[key]; + if ("undecorate" in val) { + values.push(val.undecorate()); + } else { + values.push(val); + } + } + var obj = { + name: this.name, + type: this.data.type, + value: values + }; + if (this.data.parameters) { + obj.parameters = this.data.parameters; + } + return obj; + }, + + toString: function toString() { + return ICAL.icalparser.stringifyProperty({ + name: this.name, + type: this.data.type, + value: this.data.value, + parameters: this.data.parameters + }); + }, + + getStringValue: function getStringValue() { + ICAL.helpers.dumpn("GV: " + ICAL.icalparser.stringifyValue(this.data)); + return ICAL.icalparser.stringifyValue(this.data); + }, + + setStringValue: function setStringValue(val) { + this.setValue(val, this.data.type); + // TODO force TEXT or rename method to something like setParseValue() + }, + + getFirstValue: function getValue() { + return (this.data.value ? this.data.value[0] : null); + }, + + getValues: function getValues() { + return (this.data.value ? this.data.value : []); + }, + + setValue: function setValue(aValue, aType) { + return this.setValues([aValue], aType); + }, + + setValues: function setValues(aValues, aType) { + var newValues = []; + var newType = null; + for (var key in aValues) { + var value = aValues[key]; + if (value.icalclass && value.icaltype) { + if (newType && newType != value.icaltype) { + throw new Error("All values must be of the same type!"); + } else { + newType = value.icaltype; + } + newValues.push(value); + } else { + var type; + if (aType) { + type = aType; + } else if (typeof value == "string") { + type = "TEXT"; + } else if (typeof value == "number") { + type = (Math.floor(value) == value ? "INTEGER" : "FLOAT"); + } else if (typeof value == "boolean") { + type = "BOOLEAN"; + value = (value ? "TRUE" : "FALSE"); + } else { + throw new ParserError(null, "Invalid value: " + value); + } + + if (newType && newType != type) { + throw new Error("All values must be of the same type!"); + } else { + newType = type; + } + ICAL.icalparser.validateValue(this.data, type, "" + value, true); + newValues.push(ICAL.icalparser.decorateValue(type, "" + value)); + } + } + + this.data.value = newValues; + this.data.type = newType; + return aValues; + }, + + getValueType: function getValueType() { + return this.data.type; + }, + + getName: function getName() { + return this.name; + }, + + getParameterValue: function getParameter(aName) { + var value = null; + var ucName = aName.toUpperCase(); + if (ICAL.helpers.hasKey(this.data.parameters, ucName)) { + value = this.data.parameters[ucName].value; + } + return value; + }, + + getParameterType: function getParameterType(aName) { + var type = null; + var ucName = aName.toUpperCase(); + if (ICAL.helpers.hasKey(this.data.parameters, ucName)) { + type = this.data.parameters[ucName].type; + } + return type; + }, + + setParameter: function setParameter(aName, aValue, aType) { + // TODO autodetect type by name + var ucName = aName.toUpperCase(); + ICAL.helpers.ensureKeyExists(this.data, "parameters", {}); + this.data.parameters[ucName] = { + type: aType || "TEXT", + value: aValue + }; + + if (aName == "VALUE") { + this.data.type = aValue; + // TODO revalidate value + } + }, + + countParameters: function countParmeters() { + // TODO Object.keys compatibility? + var dp = this.data.parameters; + return (dp ? Object.keys(dp).length : 0); + }, + + removeParameter: function removeParameter(aName) { + var ucName = aName.toUpperCase(); + if (ICAL.helpers.hasKey(this.data.parameters, ucName)) { + delete this.data.parameters[ucName]; + } + } + }; + + ICAL.icalproperty.fromData = function(aData) { + return new ICAL.icalproperty(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() { + ICAL.icalvalue = function icalvalue(aData, aParent, aType) { + this.parent = aParent; + this.fromData(aData, aType); + }; + + ICAL.icalvalue.prototype = { + + data: null, + parent: null, + icaltype: null, + + fromData: function icalvalue_fromData(aData, aType) { + var type = (aType || (aData && aData.type) || this.icaltype); + this.icaltype = type; + + if (aData && type) { + aData.type = type; + } + + this.data = aData; + }, + + fromString: function icalvalue_fromString(aString, aType) { + var type = aType || this.icaltype; + this.fromData(ICAL.icalparser.parseValue(aString, type), type); + }, + + undecorate: function icalvalue_undecorate() { + return this.toString(); + }, + + toString: function() { + return this.data.value.toString(); + } + }; + + ICAL.icalvalue.fromString = function icalvalue_fromString(aString, aType) { + var val = new ICAL.icalvalue(); + val.fromString(aString, aType); + return val; + }; + + ICAL.icalvalue._createFromString = function icalvalue__createFromString(ctor) { + ctor.fromString = function icalvalue_derived_fromString(aStr) { + var val = new ctor(); + val.fromString(aStr); + return val; + }; + }; + + ICAL.icalbinary = function icalbinary(aData, aParent) { + ICAL.icalvalue.call(this, aData, aParent, "BINARY"); + }; + + ICAL.icalbinary.prototype = { + + __proto__: ICAL.icalvalue.prototype, + + icaltype: "BINARY", + + decodeValue: function decodeValue() { + return this._b64_decode(this.data.value); + }, + + setEncodedValue: function setEncodedValue(val) { + this.data.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; + } + }; + ICAL.icalvalue._createFromString(ICAL.icalbinary); + + ICAL.icalutcoffset = function icalutcoffset(aData, aParent) { + ICAL.icalvalue.call(this, aData, aParent, "UTC-OFFSET"); + }; + + ICAL.icalutcoffset.prototype = { + + __proto__: ICAL.icalvalue.prototype, + + hours: null, + minutes: null, + factor: null, + + icaltype: "UTC-OFFSET", + + fromData: function fromData(aData) { + if (aData) { + this.hours = aData.hours; + this.minutes = aData.minutes; + this.factor = aData.factor; + } + }, + + toString: function toString() { + return (this.factor == 1 ? "+" : "-") + + ICAL.helpers.pad2(this.hours) + + ICAL.helpers.pad2(this.minutes); + } + }; + ICAL.icalvalue._createFromString(ICAL.icalutcoffset); +})(); +/* 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.icalperiod = function icalperiod(aData) { + this.wrappedJSObject = this; + this.fromData(aData); + }; + + ICAL.icalperiod.prototype = { + + start: null, + end: null, + duration: null, + icalclass: "icalperiod", + icaltype: "PERIOD", + + getDuration: function duration() { + if (this.duration) { + return this.duration; + } else { + return this.end.subtractDate(this.start); + } + }, + + toString: function toString() { + return this.start + "/" + (this.end || this.duration); + }, + + fromData: function fromData(data) { + if (data) { + this.start = ("start" in data ? new ICAL.icaltime(data.start) : null); + this.end = ("end" in data ? new ICAL.icaltime(data.end) : null); + this.duration = ("duration" in data ? new ICAL.icalduration(data.duration) : null); + } + } + }; + + ICAL.icalperiod.fromString = function fromString(str) { + var data = ICAL.icalparser.parseValue(str, "PERIOD"); + return ICAL.icalperiod.fromData(data); + }; + ICAL.icalperiod.fromData = function fromData(aData) { + return new ICAL.icalperiod(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() { + ICAL.icalduration = function icalduration(data) { + this.wrappedJSObject = this; + this.fromData(data); + }; + + ICAL.icalduration.prototype = { + + weeks: 0, + days: 0, + hours: 0, + minutes: 0, + seconds: 0, + isNegative: false, + icalclass: "icalduration", + icaltype: "DURATION", + + clone: function clone() { + return ICAL.icalduration.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; + } + } + + if (aData && "factor" in aData) { + this.isNegative = (aData.factor == "-1"); + } + }, + + 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; + } + } + }; + + ICAL.icalduration.fromSeconds = function icalduration_from_seconds(aSeconds) { + return (new ICAL.icalduration()).fromSeconds(); + }; + + ICAL.icalduration.fromString = function icalduration_from_string(aStr) { + var data = ICAL.icalparser.parseValue(aStr, "DURATION"); + return ICAL.icalduration.fromData(data); + }; + + ICAL.icalduration.fromData = function icalduration_from_data(aData) { + return new ICAL.icalduration(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() { + ICAL.icaltimezone = function icaltimezone(data) { + this.wrappedJSObject = this; + this.fromData(data); + }; + + ICAL.icaltimezone.prototype = { + + tzid: "", + location: "", + tznames: "", + + latitude: 0.0, + longitude: 0.0, + + component: null, + + expand_end_year: 0, + expand_start_year: 0, + + changes: null, + icalclass: "icaltimezone", + + fromData: function fromData(aData) { + var propsToCopy = ["tzid", "location", "tznames", + "latitude", "longitude"]; + for (var key in propsToCopy) { + var prop = propsToCopy[key]; + if (aData && prop in aData) { + this[prop] = aData[prop]; + } else { + this[prop] = 0; + } + } + + this.expand_end_year = 0; + this.expand_start_year = 0; + if (aData && "component" in aData) { + if (typeof aData.component == "string") { + this.component = this.componentFromString(aData.component); + } else { + this.component = ICAL.helpers.clone(aData.component, true); + } + } else { + this.component = null; + } + return this; + }, + + componentFromString: function componentFromString(str) { + this.component = ICAL.toJSON(str, true); + return this.component; + }, + + utc_offset: function utc_offset(tt) { + if (this == ICAL.icaltimezone.utc_timezone || this == ICAL.icaltimezone.local_timezone) { + return 0; + } + + this.ensure_coverage(tt.year); + + if (!this.changes || this.changes.length == 0) { + 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.find_nearby_change(tt_change); + var change_num_to_use = -1; + var step = 1; + + for (;;) { + var change = ICAL.helpers.clone(this.changes[change_num], true); + if (change.utc_offset < change.prev_utc_offset) { + ICAL.helpers.dumpn("Adjusting " + change.utc_offset); + ICAL.icaltimezone.adjust_change(change, 0, 0, 0, change.utc_offset); + } else { + ICAL.helpers.dumpn("Adjusting prev " + change.prev_utc_offset); + ICAL.icaltimezone.adjust_change(change, 0, 0, 0, + change.prev_utc_offset); + } + + var cmp = ICAL.icaltimezone._compare_change_fn(tt_change, change); + ICAL.helpers.dumpn("Compare" + cmp + " / " + change.toString()); + + 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 utc_offset_change = zone_change.utc_offset - zone_change.prev_utc_offset; + + if (utc_offset_change < 0 && change_num_to_use > 0) { + var tmp_change = ICAL.helpers.clone(zone_change, true); + ICAL.icaltimezone.adjust_change(tmp_change, 0, 0, 0, + tmp_change.prev_utc_offset); + + if (ICAL.icaltimezone._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.utc_offset; + }, + + find_nearby_change: function icaltimezone_find_nearby_change(change) { + var lower = 0, + middle = 0; + var upper = this.changes.length; + + while (lower < upper) { + middle = ICAL.helpers.trunc(lower + upper / 2); + var zone_change = this.changes[middle]; + var cmp = ICAL.icaltimezone._compare_change_fn(change, zone_change); + if (cmp == 0) { + break; + } else if (cmp > 0) { + upper = middle; + } else { + lower = middle; + } + } + + return middle; + }, + + ensure_coverage: function ensure_coverage(aYear) { + if (ICAL.icaltimezone._minimum_expansion_year == -1) { + var today = ICAL.icaltime.now(); + ICAL.icaltimezone._minimum_expansion_year = today.year; + } + + var changes_end_year = aYear; + if (changes_end_year < ICAL.icaltimezone._minimum_expansion_year) { + changes_end_year = ICAL.icaltimezone._minimum_expansion_year; + } + + changes_end_year += ICAL.icaltimezone.EXTRA_COVERAGE; + + if (changes_end_year > ICAL.icaltimezone.MAX_YEAR) { + changes_end_year = ICAL.icaltimezone.MAX_YEAR; + } + + if (!this.changes || this.expand_end_year < aYear) { + this.expand_changes(changes_end_year); + } + }, + + expand_changes: function expand_changes(aYear) { + var changes = []; + if (this.component) { + // HACK checking for component only needed for floating + // tz, which is not in core libical. + var subcomps = this.component.getAllSubcomponents(); + for (var compkey in subcomps) { + this.expand_vtimezone(subcomps[compkey], aYear, changes); + } + + this.changes = changes.concat(this.changes || []); + this.changes.sort(ICAL.icaltimezone._compare_change_fn); + } + + this.change_end_year = aYear; + }, + + expand_vtimezone: function expand_vtimezone(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.utc_offset = convert_tzoffset(aComponent.getFirstProperty("TZOFFSETTO").data); + changebase.prev_utc_offset = convert_tzoffset(aComponent.getFirstProperty("TZOFFSETFROM").data); + 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.icaltimezone.adjust_change(change, 0, 0, 0, + -change.prev_utc_offset); + changes.push(change); + } else { + var props = aComponent.getAllProperties("RDATE"); + for (var rdatekey in props) { + var rdate = props[rdatekey]; + var change = init_changes(); + change.year = rdate.time.year; + change.month = rdate.time.month; + change.day = rdate.time.day; + + if (rdate.time.isDate) { + change.hour = dtstart.hour; + change.minute = dtstart.minute; + change.second = dtstart.second; + } else { + change.hour = rdate.time.hour; + change.minute = rdate.time.minute; + change.second = rdate.time.second; + + if (rdate.time.zone == ICAL.icaltimezone.utc_timezone) { + ICAL.icaltimezone.adjust_change(change, 0, 0, 0, + -change.prev_utc_offset); + } + } + + changes.push(change); + } + + var rrule = aComponent.getFirstProperty("RRULE").getFirstValue(); + // TODO multiple rrules? + + var change = init_changes(); + + if (rrule.until && rrule.until.zone == ICAL.icaltimezone.utc_timezone) { + rrule.until.adjust(0, 0, 0, change.prev_utc_offset); + rrule.until.zone = ICAL.icaltimezone.local_timezone; + } + + 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.icaltimezone.adjust_change(change, 0, 0, 0, + -change.prev_utc_offset); + changes.push(change); + } + } + + return changes; + }, + + toString: function toString() { + return (this.tznames ? this.tznames : this.tzid); + } + + }; + + ICAL.icaltimezone._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.icaltimezone.convert_time = function icaltimezone_convert_time(tt, from_zone, to_zone) { + if (tt.isDate || + from_zone.tzid == to_zone.tzid || + from_zone == ICAL.icaltimezone.local_timezone || + to_zone == ICAL.icaltimezone.local_timezone) { + tt.zone = to_zone; + return tt; + } + + var utc_offset = from_zone.utc_offset(tt); + tt.adjust(0, 0, 0, - utc_offset); + + utc_offset = to_zone.utc_offset(tt); + tt.adjust(0, 0, 0, utc_offset); + + return null; + }; + + ICAL.icaltimezone.fromData = function icaltimezone_fromData(aData) { + var tt = new ICAL.icaltimezone(); + return tt.fromData(aData); + }; + + ICAL.icaltimezone.utc_timezone = ICAL.icaltimezone.fromData({ + tzid: "UTC" + }); + ICAL.icaltimezone.local_timezone = ICAL.icaltimezone.fromData({ + tzid: "floating" + }); + + ICAL.icaltimezone.adjust_change = function icaltimezone_adjust_change(change, days, hours, minutes, seconds) { + return ICAL.icaltime.prototype.adjust.call(change, days, hours, minutes, seconds); + }; + + ICAL.icaltimezone._minimum_expansion_year = -1; + ICAL.icaltimezone.MAX_YEAR = 2035; // TODO this is because of time_t, which we don't need. Still usefull? + ICAL.icaltimezone.EXTRA_COVERAGE = 5; +})(); +/* 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.icaltime = function icaltime(data) { + this.wrappedJSObject = this; + this.fromData(data); + }; + + ICAL.icaltime.prototype = { + + year: 0, + month: 1, + day: 1, + + hour: 0, + minute: 0, + second: 0, + + isDate: false, + zone: null, + + auto_normalize: false, + icalclass: "icaltime", + icaltype: "DATE-TIME", + + clone: function icaltime_clone() { + return new ICAL.icaltime(this); + }, + + reset: function icaltime_reset() { + this.fromData(ICAL.icaltime.epoch_time); + this.zone = ICAL.icaltimezone.utc_timezone; + }, + + 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.icalparser.parseValue(str, "DATE"); + data.isDate = true; + } catch (e) { + data = ICAL.icalparser.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.icaltimzone.utc_timezone; + 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.icaltimezone.local_timezone; + 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) { + // TODO given we're switching formats, this may not be needed + var old_auto_normalize = this.auto_normalize; + this.auto_normalize = false; + + var propsToCopy = { + year: 0, + month: 1, + day: 1, + hour: 0, + minute: 0, + second: 0 + }; + for (var key in propsToCopy) { + if (aData && key in aData) { + this[key] = aData[key]; + } else { + this[key] = propsToCopy[key]; + } + } + 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 && aData.timezone == "Z") { + this.zone = ICAL.icaltimezone.utc_timezone; + } + if (aData && "zone" in aData) { + this.zone = aData.zone; + } + + if (!this.zone) { + this.zone = ICAL.icaltimezone.local_timezone; + } + + if (aData && "auto_normalize" in aData) { + this.auto_normalize = aData.auto_normalize; + } else { + this.auto_normalize = old_auto_normalize; + } + if (this.auto_normalize) { + this.normalize(); + } + return this; + }, + + day_of_week: function icaltime_day_of_week() { + // 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; + }, + + day_of_year: function icaltime_day_of_year() { + var is_leap = (ICAL.icaltime.is_leap_year(this.year) ? 1 : 0); + var diypm = ICAL.icaltime._days_in_year_passed_month; + return diypm[is_leap][this.month - 1] + this.day; + }, + + start_of_week: function start_of_week() { + var result = this.clone(); + result.day -= this.day_of_week() - 1; + return result.normalize(); + }, + + end_of_week: function end_of_week() { + var result = this.clone(); + result.day += 7 - this.day_of_week(); + return result.normalize(); + }, + + start_of_month: function start_of_month() { + var result = this.clone(); + result.day = 1; + result.isDate = true; + result.hour = 0; + result.minute = 0; + result.second = 0; + return result; + }, + + end_of_month: function end_of_month() { + var result = this.clone(); + result.day = ICAL.icaltime.days_in_month(result.month, result.year); + result.isDate = true; + result.hour = 0; + result.minute = 0; + result.second = 0; + return result; + }, + + start_of_year: function start_of_year() { + var result = this.clone(); + result.day = 1; + result.month = 1; + result.isDate = true; + result.hour = 0; + result.minute = 0; + result.second = 0; + return result; + }, + + end_of_year: function end_of_year() { + var result = this.clone(); + result.day = 31; + result.month = 12; + result.isDate = true; + result.hour = 0; + result.minute = 0; + result.second = 0; + return result; + }, + + start_doy_week: function start_doy_week(aFirstDayOfWeek) { + var firstDow = aFirstDayOfWeek || ICAL.icaltime.SUNDAY; + var delta = this.day_of_week() - firstDow; + if (delta < 0) delta += 7; + return this.day_of_year() - delta; + }, + + nth_weekday: function icaltime_nth_weekday(aDayOfWeek, aPos) { + var days_in_month = ICAL.icaltime.days_in_month(this.month, this.year); + var weekday; + var pos = aPos; + + var otherday = this.clone(); + + if (pos >= 0) { + otherday.day = 1; + var start_dow = otherday.day_of_week(); + + if (pos != 0) { + pos--; + } + + weekday = aDayOfWeek - start_dow + 1; + + if (weekday <= 0) { + weekday += 7; + } + } else { + otherday.day = days_in_month; + var end_dow = otherday.day_of_week(); + + pos++; + + weekday = (end_dow - dow); + + if (weekday < 0) { + weekday += 7; + } + + weekday = days_in_month - weekday; + } + + weekday += pos * 7; + + return weekday; + }, + + week_number: function week_number(aWeekStart) { + // This function courtesty of Julian Bucknall, published under the MIT license + // http://www.boyet.com/articles/publishedarticles/calculatingtheisoweeknumb.html + var doy = this.day_of_year(); + var dow = this.day_of_week(); + var year = this.year; + var week1; + + var dt = this.clone(); + dt.isDate = true; + var first_dow = dt.day_of_week(); + var isoyear = this.year; + + if (dt.month == 12 && dt.day > 28) { + week1 = ICAL.icaltime.week_one_starts(isoyear + 1, aWeekStart); + if (dt.compare(week1) < 0) { + week1 = ICAL.icaltime.week_one_starts(isoyear, aWeekStart); + } else { + isoyear++; + } + } else { + week1 = ICAL.icaltime.week_one_starts(isoyear, aWeekStart); + if (dt.compare(week1) < 0) { + week1 = ICAL.icaltime.week_one_starts(--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); + + this.second += mult * aDuration.seconds; + this.minute += mult * aDuration.minutes; + this.hour += mult * aDuration.hours; + this.day += mult * aDuration.days; + this.day += mult * 7 * aDuration.weeks; + + this.normalize(); + }, + + subtractDate: function icaltime_subtract(aDate) { + function leap_years_until(aYear) { + return ICAL.helpers.trunc(aYear / 4) - + ICAL.helpers.trunc(aYear / 100) + + ICAL.helpers.trunc(aYear / 400); + } + + function leap_years_between(aStart, aEnd) { + if (aStart >= aEnd) { + return 0; + } else { + return leap_years_until(aEnd - 1) - leap_years_until(aStart); + } + } + var dur = new ICAL.icalduration(); + + dur.seconds = this.second - aDate.second; + dur.minutes = this.minute - aDate.minute; + dur.hours = this.hour - aDate.hour; + + if (this.year == aDate.year) { + var this_doy = this.day_of_year(); + var that_doy = aDate.day_of_year(); + dur.days = this_doy - that_doy; + } else if (this.year < aDate.year) { + var days_left_thisyear = 365 + + (ICAL.icaltime.is_leap_year(this.year) ? 1 : 0) - + this.day_of_year(); + + dur.days -= days_left_thisyear + aDate.day_of_year(); + dur.days -= leap_years_between(this.year + 1, aDate.year); + dur.days -= 365 * (aDate.year - this.year - 1); + } else { + var days_left_thatyear = 365 + + (ICAL.icaltime.is_leap_year(aDate.year) ? 1 : 0) - + aDate.day_of_year(); + + dur.days += days_left_thatyear + this.day_of_year(); + dur.days += leap_years_between(aDate.year + 1, this.year); + dur.days += 365 * (this.year - aDate.year - 1); + } + + return dur.normalize(); + }, + + compare: function icaltime_compare(other) { + function cmp(attr) { + return ICAL.icaltime._cmp_attr(a, b, attr); + } + + if (!other) return 0; + + if (this.isDate || other.isDate) { + return this.compare_date_only_tz(other, this.zone); + } + + var target_zone; + if (this.zone == ICAL.icaltimezone.local_timezone || + other.zone == ICAL.icaltimezone.local_timezone) { + target_zone = ICAL.icaltimezone.local_timezone; + } else { + target_zone = ICAL.icaltimezone.utc_timezone; + } + + var a = this.convert_to_zone(target_zone); + var b = other.convert_to_zone(target_zone); + var rc = 0; + + if ((rc = cmp("year")) != 0) return rc; + if ((rc = cmp("month")) != 0) return rc; + if ((rc = cmp("day")) != 0) return rc; + + if (a.isDate && b.isDate) { + // If both are dates, we are done + return 0; + } else if (b.isDate) { + // If b is a date, then a is greater + return 1; + } else if (a.isDate) { + // If a is a date, then b is greater + return -1; + } + + if ((rc = cmp("hour")) != 0) return rc; + if ((rc = cmp("minute")) != 0) return rc; + if ((rc = cmp("second")) != 0) return rc; + + // Now rc is 0 and the dates are equal + return rc; + }, + + compare_date_only_tz: function icaltime_compare_date_only_tz(other, tz) { + function cmp(attr) { + return ICAL.icaltime._cmp_attr(a, b, attr); + } + var a = this.convert_to_zone(tz); + var b = other.convert_to_zone(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; + }, + + convert_to_zone: function convert_to_zone(zone) { + var copy = this.clone(); + var zone_equals = (this.zone.tzid == zone.tzid); + + if (!this.isDate && !zone_equals) { + ICAL.icaltimezone.convert_time(copy, this.zone, zone); + } + + copy.zone = zone; + return copy; + }, + + utc_offset: function utc_offset() { + if (this.zone == ICAL.icaltimezone.local_timezone || + this.zone == ICAL.icaltimezone.utc_timezone) { + return 0; + } else { + return this.zone.utc_offset(this); + } + }, + + toString: function toString() { + return ("0000" + this.year).substr(-4) + + ("00" + this.month).substr(-2) + + ("00" + this.day).substr(-2) + + (this.isDate ? "" : + "T" + + ("00" + this.hour).substr(-2) + + ("00" + this.minute).substr(-2) + + ("00" + this.second).substr(-2) + + (this.zone && this.zone.tzid == "UTC" ? "Z" : "") + ); + }, + + toJSDate: function toJSDate() { + if (this.zone == ICAL.icaltimezone.local_timezone) { + 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 { + var utcDate = this.convert_to_zone(ICAL.icaltimezone.utc_timezone); + if (this.isDate) { + return Date.UTC(this.year, this.month - 1, this.day); + } else { + return Date.UTC(this.year, this.month - 1, this.day, + this.hour, this.minute, this.second, 0); + } + } + }, + + normalize: function icaltime_normalize() { + if (this.isDate) { + this.hour = 0; + this.minute = 0; + this.second = 0; + } + this.icaltype = (this.isDate ? "DATE" : "DATE-TIME"); + + this.adjust(0, 0, 0, 0); + return this; + }, + + adjust: function icaltime_adjust(aExtraDays, aExtraHours, + aExtraMinutes, aExtraSeconds) { + var second, minute, hour, day; + var minutes_overflow, hours_overflow, days_overflow = 0, + years_overflow = 0; + var days_in_month; + + if (!this.isDate) { + second = this.second + aExtraSeconds; + this.second = second % 60; + minutes_overflow = ICAL.helpers.trunc(second / 60); + if (this.second < 0) { + this.second += 60; + minutes_overflow--; + } + + minute = this.minute + aExtraMinutes + minutes_overflow; + this.minute = minute % 60; + hours_overflow = ICAL.helpers.trunc(minute / 60); + if (this.minute < 0) { + this.minute += 60; + hours_overflow--; + } + + hour = this.hour + aExtraHours + hours_overflow; + this.hour = hour % 24; + days_overflow = ICAL.helpers.trunc(hour / 24); + if (this.hour < 0) { + this.hour += 24; + days_overflow--; + } + } + + // Adjust month and year first, because we need to know what month the day + // is in before adjusting it. + if (this.month > 12) { + years_overflow = ICAL.helpers.trunc((this.month - 1) / 12); + } else if (this.month < 1) { + years_overflow = ICAL.helpers.trunc(this.month / 12) - 1; + } + + this.year += years_overflow; + this.month -= 12 * years_overflow; + + // Now take care of the days (and adjust month if needed) + day = this.day + aExtraDays + days_overflow; + if (day > 0) { + for (;;) { + var days_in_month = ICAL.icaltime.days_in_month(this.month, this.year); + if (day <= days_in_month) { + break; + } + + this.month++; + if (this.month > 12) { + this.year++; + this.month = 1; + } + + day -= days_in_month; + } + } else { + while (day <= 0) { + if (this.month == 1) { + this.year--; + this.month = 12; + } else { + this.month--; + } + + day += ICAL.icaltime.days_in_month(this.month, this.year); + } + } + + this.day = day; + return this; + }, + + fromUnixTime: function fromUnixTime(seconds) { + var epoch = ICAL.icaltime.epoch_time.clone(); + epoch.adjust(0, 0, 0, seconds); + this.fromData(epoch); + this.zone = ICAL.icaltimezone.utc_timezone; + }, + + toUnixTime: function toUnixTime() { + var dur = this.subtractDate(ICAL.icaltime.epoch_time); + return dur.toSeconds(); + } + }; + + (function setupNormalizeAttributes() { + // This needs to run before any instances are created! + function addAutoNormalizeAttribute(attr, mattr) { + ICAL.icaltime.prototype[mattr] = ICAL.icaltime.prototype[attr]; + + Object.defineProperty(ICAL.icaltime.prototype, attr, { + get: function() { + return this[mattr]; + }, + set: function(val) { + this[mattr] = val; + if (this.auto_normalize) { + var old_normalize = this.auto_normalize; + this.auto_normalize = false; + this.normalize(); + this.auto_normalize = old_normalize; + } + return val; + } + }); + + } + + if ("defineProperty" in Object) { + addAutoNormalizeAttribute("year", "mYear"); + addAutoNormalizeAttribute("month", "mMonth"); + addAutoNormalizeAttribute("day", "mDay"); + addAutoNormalizeAttribute("hour", "mHour"); + addAutoNormalizeAttribute("minute", "mMinute"); + addAutoNormalizeAttribute("second", "mSecond"); + addAutoNormalizeAttribute("isDate", "mIsDate"); + + ICAL.icaltime.prototype.auto_normalize = true; + } + })(); + + ICAL.icaltime.days_in_month = function icaltime_days_in_month(month, year) { + var _days_in_month = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; + var days = 30; + + if (month < 1 || month > 12) return days; + + days = _days_in_month[month]; + + if (month == 2) { + days += ICAL.icaltime.is_leap_year(year); + } + + return days; + }; + + ICAL.icaltime.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.icaltime.from_day_of_year = function icaltime_from_day_of_year(aDayOfYear, aYear) { + var year = aYear; + var doy = aDayOfYear; + var tt = new ICAL.icaltime(); + tt.auto_normalize = false; + var is_leap = (ICAL.icaltime.is_leap_year(year) ? 1 : 0); + + if (doy < 1) { + year--; + is_leap = (ICAL.icaltime.is_leap_year(year) ? 1 : 0); + doy += ICAL.icaltime._days_in_year_passed_month[is_leap][12]; + } else if (doy > ICAL.icaltime._days_in_year_passed_month[is_leap][12]) { + is_leap = (ICAL.icaltime.is_leap_year(year) ? 1 : 0); + doy -= ICAL.icaltime._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.icaltime._days_in_year_passed_month[is_leap][month]) { + tt.month = month + 1; + tt.day = doy - ICAL.icaltime._days_in_year_passed_month[is_leap][month]; + break; + } + } + + tt.auto_normalize = true; + return tt; + }; + + ICAL.icaltime.fromString = function fromString(str) { + var tt = new ICAL.icaltime(); + return tt.fromString(str); + }; + + ICAL.icaltime.fromJSDate = function fromJSDate(aDate, useUTC) { + var tt = new ICAL.icaltime(); + return tt.fromJSDate(aDate, useUTC); + }; + + ICAL.icaltime.fromData = function fromData(aData) { + var t = new ICAL.icaltime(); + return t.fromData(aData); + }; + + ICAL.icaltime.now = function icaltime_now() { + return ICAL.icaltime.fromJSDate(new Date(), false); + }; + + ICAL.icaltime.week_one_starts = function week_one_starts(aYear, aWeekStart) { + var t = ICAL.icaltime.fromData({ + year: aYear, + month: 1, + day: 4, + isDate: true + }); + + var fourth_dow = t.day_of_week(); + t.day += (1 - fourth_dow) + ((aWeekStart || ICAL.icaltime.SUNDAY) - 1); + return t; + }; + + ICAL.icaltime.epoch_time = ICAL.icaltime.fromData({ + year: 1970, + month: 1, + day: 1, + hour: 0, + minute: 0, + second: 0, + isDate: false, + timezone: "Z" + }); + + ICAL.icaltime._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.icaltime._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.icaltime.SUNDAY = 1; + ICAL.icaltime.MONDAY = 2; + ICAL.icaltime.TUESDAY = 3; + ICAL.icaltime.WEDNESDAY = 4; + ICAL.icaltime.THURSDAY = 5; + ICAL.icaltime.FRIDAY = 6; + ICAL.icaltime.SATURDAY = 7; +})(); +/* 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.icalrecur = function icalrecur(data) { + this.wrappedJSObject = this; + this.parts = {}; + this.fromData(data); + }; + + ICAL.icalrecur.prototype = { + + parts: null, + + interval: 1, + wkst: ICAL.icaltime.MONDAY, + until: null, + count: null, + freq: null, + icalclass: "icalrecur", + icaltype: "RECUR", + + iterator: function(aStart) { + return new icalrecur_iterator(this, aStart); + }, + + clone: function clone() { + return ICAL.icalrecur.fromData(this); + //return ICAL.icalrecur.fromIcalProperty(this.toIcalProperty()); + }, + + is_finite: function isfinite() { + return (this.count || this.until); + }, + + is_by_count: 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) { + ICAL.helpers.dumpn("GNO: " + aRecurrenceId + " / " + aStartTime); + var iter = this.iterator(aStartTime); + var next, cdt; + + do { + next = iter.next(); + ICAL.helpers.dumpn("Checking " + next + " <= " + aRecurrenceId); + } while (next && next.compare(aRecurrenceId) <= 0); + + if (next && aRecurrenceId.zone) { + next.zone = aRecurrenceId.zone; + } + + return next; + }, + + fromData: function fromData(aData) { + var propsToCopy = ["freq", "count", "wkst", "interval"]; + for (var key in propsToCopy) { + var prop = propsToCopy[key]; + if (aData && prop.toUpperCase() in aData) { + this[prop] = aData[prop.toUpperCase()]; + // TODO casing sucks, fix the parser! + } else if (aData && prop in aData) { + this[prop] = aData[prop]; + // TODO casing sucks, fix the parser! + } + } + + if (aData && "until" in aData && aData.until) { + this.until = aData.until.clone(); + } + + var partsToCopy = ["BYSECOND", "BYMINUTE", "BYHOUR", "BYDAY", + "BYMONTHDAY", "BYYEARDAY", "BYWEEKNO", + "BYMONTH", "BYSETPOS"]; + this.parts = {}; + if (aData) { + for (var key in partsToCopy) { + var prop = partsToCopy[key]; + if (prop in aData) { + this.parts[prop] = aData[prop]; + // TODO casing sucks, fix the parser! + } + } + // TODO oh god, make it go away! + if (aData.parts) { + for (var key in partsToCopy) { + var prop = partsToCopy[key]; + if (prop in aData.parts) { + this.parts[prop] = aData.parts[prop]; + // TODO casing sucks, fix the parser! + } + } + } + } + return this; + }, + + 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]; + } + return str; + }, + + toIcalProperty: function toIcalProperty() { + try { + var valueData = { + name: this.isNegative ? "EXRULE" : "RRULE", + type: "RECUR", + value: [this.toString()] + // TODO more props? + }; + return ICAL.icalproperty.fromData(valueData); + } catch (e) { + ICAL.helpers.dumpn("EICALPROP: " + this.toString() + "//" + e); + ICAL.helpers.dumpn(e.stack); + return null; + } + + return null; + }, + + fromIcalProperty: function fromIcalProperty(aProp) { + var propval = aProp.getFirstValue(); + this.fromData(propval); + this.parts = ICAL.helpers.clone(propval.parts, true); + if (aProp.name == "EXRULE") { + this.isNegative = true; + } else if (aProp.name == "RRULE") { + this.isNegative = false; + } else { + throw new Error("Invalid Property " + aProp.name + " passed"); + } + } + }; + + ICAL.icalrecur.fromData = function icalrecur_fromData(data) { + return (new ICAL.icalrecur(data)); + } + + ICAL.icalrecur.fromString = function icalrecur_fromString(str) { + var data = ICAL.icalparser.parseValue(str, "RECUR"); + return ICAL.icalrecur.fromData(data); + }; + + ICAL.icalrecur.fromIcalProperty = function icalrecur_fromIcalProperty(prop) { + var recur = new ICAL.icalrecur(); + recur.fromIcalProperty(prop); + return recur; + }; + + function icalrecur_iterator(aRule, aStart) { + this.rule = aRule; + this.dtstart = aStart; + this.by_data = ICAL.helpers.clone(aRule.parts, true); + this.days = []; + this.init(); + } + + icalrecur_iterator.prototype = { + + rule: null, + dtstart: null, + last: null, + occurrence_number: 0, + by_indices: null, + by_data: null, + + days: null, + days_index: 0, + + init: function icalrecur_iterator_init() { + this.last = this.dtstart.clone(); + var parts = this.by_data; + + this.by_indices = { + "BYSECOND": 0, + "BYMINUTE": 0, + "BYHOUR": 0, + "BYDAY": 0, + "BYMONTH": 0, + "BYWEEKNO": 0, + "BYMONTHDAY": 0 + }; + + 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.rule_day_of_week(parts.BYDAY[0]); + var pos = parts[0]; + var rule_dow = parts[1]; + var dow = rule_dow - this.last.day_of_week(); + if ((this.last.day_of_week() < rule_dow && dow >= 0) || dow < 0) { + // Initial time is after first day of BYDAY data + this.last.day += dow; + this.last.normalize(); + } + } else { + var wkMap = icalrecur_iterator._wkdayMap[this.dtstart.day_of_week()]; + parts.BYDAY = [wkMap]; + } + } + + 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.icaltime.from_day_of_year(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.rule_day_of_week(coded_day); + var pos = parts[0]; + var dow = parts[1]; + + var days_in_month = ICAL.icaltime.days_in_month(this.last.month, this.last.year); + var poscount = 0; + + if (pos >= 0) { + for (this.last.day = 1; this.last.day <= days_in_month; this.last.day++) { + if (this.last.day_of_week() == dow) { + if (++poscount == pos || pos == 0) { + break; + } + } + } + } else { + pos = -pos; + for (this.last.day = days_in_month; this.last.day != 0; this.last.day--) { + if (this.last.day_of_week() == dow) { + if (++poscount == pos) { + break; + } + } + } + } + + if (this.last.day > days_in_month || 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 days_in_month = ICAL.icaltime.days_in_month(this.last.month, this.last.year); + this.last.day = days_in_month + this.last.day + 1; + } + + this.last.normalize(); + } + }, + + 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)) { + 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) { + 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; + this.last.normalize(); + + 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; + }, + + 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")) { + var days_in_month = ICAL.icaltime.days_in_month(this.last.month, this.last.year); + var notFound = true; + var day; + + for (day = last.day + 1; notFound && day <= days_in_month; day++) { + for (var dayIdx = 0; dayIdx < this.by_data.BYDAY.length; dayIdx++) { + for (var mdIdx = 0; mdIdx < this.by_data.BYMONTHDAY.length; mdIdx++) { + var parts = this.rule_day_of_week(this.by_data.BYDAY[dayIdx]); + var pos = parts[0]; + var dow = parts[1]; + var mday = this.by_data.BYMONTHDAY[mdIdx]; + + this.last.day = day; + var this_dow = this.last.day_of_week(); + + if ((pos == 0 && dow == this_dow && mday == day) || + (this.last.nth_weekday(dow, pos))) { + notFound = false; + } + } + } + } + if (day > days_in_month) { + this.last.day = 1; + this.increment_month(); + this.last.day--; + data_valid = 0; + } + + } else if (this.has_by_data("BYDAY")) { + var days_in_month = ICAL.icaltime.days_in_month(this.last.month, this.last.year); + var setpos = 0; + + if (this.has_by_data("BYSETPOS")) { + var lastday = this.last.day; + for (var day = 1; day <= days_in_month; 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 <= days_in_month; 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)) { + found = 1; + break; + } + } + } + + data_valid = found; + + if (day > days_in_month) { + 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 days_in_month = ICAL.icaltime.days_in_month(this.last.month, this.last.year); + + var day = this.by_data.BYMONTHDAY[this.by_indices.BYMONTHDAY]; + + if (day < 0) { + day = days_in_month + day + 1; + } + + if (day > days_in_month) { + this.last.day = 1; + data_valid = this.is_day_in_byday(this.last); + } + + this.last.day = day; + } else { + this.last.day = this.by_data.BYMONTHDAY[0]; + this.increment_month(); + var days_in_month = ICAL.icaltime.days_in_month(this.last.month, this.last.year); + this.last.day = Math.min(this.last.day, days_in_month); + } + + 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.icaltime(); + tt.auto_normalize = false; + this.by_indices.BYDAY++; + + if (this.by_indices.BYDAY == 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.rule_day_of_week(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 start_of_week = tt.start_doy_week(this.rule.wkst); + + if (dow + start_of_week < 1) { + // The selected date is in the previous year + if (!end_of_data) { + continue; + } + } + + var next = ICAL.icaltime.from_day_of_year(start_of_week + dow, + this.last.year); + + this.last.day = next.day; + this.last.month = next.month; + this.last.year = next.year; + + 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.icaltime.from_day_of_year(this.days[this.days_index], + this.last.year); + + this.last.day = next.day; + this.last.month = next.month; + + return 1; + }, + + rule_day_of_week: function rule_day_of_week(dow) { + var dowMap = { + SU: 1, + MO: 2, + TU: 3, + WE: 4, + TH: 5, + FR: 6, + SA: 7 + }; + var matches = dow.match(/([+-]?[0-9])?(MO|TU|WE|TH|FR|SA|SU)/); + if (matches) { + return [parseInt(matches[1] || 0, 10), dowMap[matches[2]]] || 0; + } 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 days_in_month = ICAL.icaltime.days_in_month(this.last.month, this.last.year); + this.last.day++; + + if (this.last.day > days_in_month) { + this.last.day -= days_in_month; + this.increment_month(); + } + } + }, + + increment_month: function increment_month() { + if (this.has_by_data("BYMONTH")) { + this.by_indices.BYMONTH++; + + if (this.by_indices.BYMONTH == this.by_data.BYMONTH.length) { + this.by_indices.BYMONTH = 0; + this.increment_year(1); + } + + this.last.month = this.by_data.BYMONTH[this.by_indices.BYMONTH]; + } else { + var inc; + if (this.rule.freq == "MONTHLY") { + this.last.month += this.rule.interval; + } else { + this.last.month++; + } + + this.last.month--; + var years = ICAL.helpers.trunc(this.last.month / 12); + 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.icaltime(); + 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.week_number(this.rule.wkst); + t.day = ICAL.icaltime.days_in_month(month, aYear); + var last_week = t.week_number(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.day_of_year()); + } 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.day_of_year()); + } + } 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.day_of_year()); + } + } 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.day_of_year()); + } + } + } 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) { + var days_in_month = ICAL.icaltime.days_in_month(month, aYear); + + t.year = aYear; + t.month = this.by_data.BYMONTH[monthkey]; + t.day = 1; + t.isDate = true; + + var first_dow = t.day_of_week(); + var doy_offset = t.day_of_year() - 1; + + t.day = days_in_month; + var last_dow = t.day_of_week(); + + if (this.has_by_data("BYSETPOS")) { + var set_pos_counter = 0; + var by_month_day = []; + for (var day = 1; day <= days_in_month; 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) { + var coded_day = this.by_data.BYDAY[daycodedkey]; + var parts = this.rule_day_of_week(coded_day); + var dow = parts[0]; + var pos = parts[1]; + + var first_matching_day = ((dow + 7 - first_dow) % 7) + 1; + var last_matching_day = days_in_month - ((last_dow + 7 - dow) % 7); + + if (pos == 0) { + for (var day = first_matching_day; day <= days_in_month; day += 7) { + this.days.push(doy_offset + day); + } + } else if (pos > 0) { + month_day = first_matching_day + (pos - 1) * 7; + + if (month_day <= days_in_month) { + 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.icaltime.from_day_of_year(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.icaltime.from_day_of_year(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.icaltime.from_day_of_year(day, aYear); + var weekno = tt.week_number(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.day_of_week(); + + tmp.month = 12; + tmp.day = 31; + tmp.isDate = true; + + var end_dow = tmp.day_of_week(); + var end_year_day = tmp.day_of_year(); + + for (var daykey in this.by_data.BYDAY) { + var day = this.by_data.BYDAY[daykey]; + var parts = this.rule_day_of_week(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.rule_day_of_week(day); + var pos = parts[0]; + var dow = parts[1]; + var this_dow = tt.day_of_week(); + + if ((pos == 0 && dow == this_dow) || + (tt.nth_weekday(dow, pos) == tt.day)) { + return 1; + } + } + + return 0; + }, + + check_set_position: function check_set_position(aPos) { + return ("BYSETPOS" in this.by_data && + this.by_data.BYSETPOS.indexOf(aPos)); + }, + + 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.rule_day_of_week(aRules[j])[1]; + var two = this.rule_day_of_week(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) { + for (var bydatakey in this.by_data[aRuleType]) { + if (this.by_data[aRuleType][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.day_of_week(); + var weekNo = this.last.week_number(this.rule.wkst); + var doy = this.last.day_of_year(); + + 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", 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; + } + }; + + icalrecur_iterator._wkdayMap = ["", "SU", "MO", "TU", "WE", "TH", "FR", "SA"]; + + 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; +})(); +/* 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.foldLength = 75; + ICAL.newLineChar = "\n"; + + /** + * Return a parsed ICAL object to the ICAL format. + * + * @param {Object} object parsed ical string. + * @return {String} ICAL string. + */ + ICAL.stringify = function ICALStringify(object) { + return ICAL.serializer.serializeToIcal(object); + }; + + /** + * Parse an ICAL object or string. + * + * @param {String|Object} ical ical string or pre-parsed object. + * @param {Boolean} decorate when true decorates object data types. + * + * @return {Object|ICAL.icalcomponent} The raw data or decorated icalcomponent. + */ + ICAL.parse = function ICALParse(ical) { + var state = ICAL.helpers.initState(ical, 0); + + while (state.buffer.length) { + var line = ICAL.helpers.unfoldline(state); + var lexState = ICAL.helpers.initState(line, state.lineNr); + if (line.match(/^\s*$/) && state.buffer.match(/^\s*$/)) { + break; + } + + var lineData = ICAL.icalparser.lexContentLine(lexState); + ICAL.icalparser.parseContentLine(state, lineData); + state.lineNr++; + } + + return state.currentData; + }; +}()); -- cgit