/* 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 = {}; ICAL.foldLength = 75; ICAL.newLineChar = '\r\n'; /** * Helper functions used in various places within ical.js */ ICAL.helpers = { initState: function initState(aLine, aLineNr) { return { buffer: aLine, line: aLine, lineNr: aLineNr, character: 0, currentData: null, parentData: [] }; }, initComponentData: function initComponentData(aName) { return { name: aName, type: "COMPONENT", value: [] }; }, /** * Creates or returns a class instance * of a given type with the initialization * data if the data is not already an instance * of the given type. * * * Example: * * var time = new ICAL.icaltime(...); * var result = ICAL.helpers.formatClassType(time, ICAL.icaltime); * * (result instanceof ICAL.icaltime) * // => true * * result = ICAL.helpers.formatClassType({}, ICAL.icaltime); * (result isntanceof ICAL.icaltime) * // => true * * * @param {Object} data object initialization data. * @param {Object} type object type (like ICAL.icaltime). */ formatClassType: function formatClassType(data, type) { if (typeof(data) === 'undefined') return undefined; if (data instanceof type) { return data; } return new type(data); }, /** * Identical to index of but will only match values * when they are not preceded by a backslash char \\\ * * @param {String} buffer string value. * @param {String} search value. * @param {Numeric} pos start position. */ unescapedIndexOf: function(buffer, search, pos) { while ((pos = buffer.indexOf(search, pos)) !== -1) { if (pos > 0 && buffer[pos - 1] === '\\') { pos += 1; } else { return pos; } } return -1; }, binsearchInsert: function(list, seekVal, cmpfunc) { if (!list.length) return 0; var low = 0, high = list.length - 1, mid, cmpval; while (low <= high) { mid = low + Math.floor((high - low) / 2); cmpval = cmpfunc(seekVal, list[mid]); if (cmpval < 0) high = mid - 1; else if (cmpval > 0) low = mid + 1; else break; } if (cmpval < 0) return mid; // insertion is displacing, so use mid outright. else if (cmpval > 0) return mid + 1; else return mid; }, dumpn: function() { if (!ICAL.debug) { return null; } if (typeof (console) !== 'undefined' && 'log' in console) { ICAL.helpers.dumpn = function consoleDumpn(input) { return console.log(input); } } else { ICAL.helpers.dumpn = function geckoDumpn(input) { dump(input + '\n'); } } return ICAL.helpers.dumpn(arguments[0]); }, mixin: function(obj, data) { if (data) { for (var k in data) { obj[k] = data[k]; } } return obj; }, isArray: function(o) { return o && (o instanceof Array || typeof o == "array"); }, clone: function(aSrc, aDeep) { if (!aSrc || typeof aSrc != "object") { return aSrc; } else if (aSrc instanceof Date) { return new Date(aSrc.getTime()); } else if ("clone" in aSrc) { return aSrc.clone(); } else if (ICAL.helpers.isArray(aSrc)) { var result = []; for (var i = 0; i < aSrc.length; i++) { result.push(aDeep ? ICAL.helpers.clone(aSrc[i], true) : aSrc[i]); } return result; } else { var result = {}; for (var name in aSrc) { if (aSrc.hasOwnProperty(name)) { this.dumpn("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) { if (typeof(data) !== 'string') { data = String(data); } var len = data.length; switch (len) { case 0: return '00'; case 1: return '0' + data; default: return data; } }, trunc: function trunc(number) { return (number < 0 ? Math.ceil(number) : Math.floor(number)); } }; /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. * Portions Copyright (C) Philipp Kewisch, 2011-2012 */ (typeof(ICAL) === 'undefined')? ICAL = {} : ''; ICAL.design = (function() { 'use strict'; var ICAL_NEWLINE = /\\\\|\\;|\\,|\\[Nn]/g; function DecorationError() { Error.apply(this, arguments); } DecorationError.prototype = { __proto__: Error.prototype }; function isStrictlyNaN(number) { return typeof(number) === 'number' && isNaN(number); } /** * Parses a string value that is expected to be an * integer, when the valid is not an integer throws * a decoration error. * * @param {String} string raw input. * @return {Number} integer. */ function strictParseInt(string) { var result = parseInt(string, 10); if (isStrictlyNaN(result)) { throw new DecorationError( 'Could not extract integer from "' + string + '"' ); } return result; } function replaceNewlineReplace(string) { switch (string) { case "\\\\": return "\\"; case "\\;": return ";"; case "\\,": return ","; case "\\n": case "\\N": return "\n"; default: return string; } } function replaceNewline(value) { // avoid regex when possible. if (value.indexOf('\\') === -1) { return value; } return value.replace(ICAL_NEWLINE, replaceNewlineReplace); } /** * Design data used by the parser to decide if data is semantically correct */ var design = { DecorationError: DecorationError, defaultType: 'text', param: { // Although the syntax is DQUOTE uri DQUOTE, I don't think we should // enfoce anything aside from it being a valid content line. // "ALTREP": { ... }, // CN just wants a param-value // "CN": { ... } "cutype": { values: ["INDIVIDUAL", "GROUP", "RESOURCE", "ROOM", "UNKNOWN"], allowXName: true, allowIanaToken: true }, "delegated-from": { valueType: "cal-address", multiValue: "," }, "delegated-to": { valueType: "cal-address", multiValue: "," }, // "DIR": { ... }, // See ALTREP "encoding": { values: ["8BIT", "BASE64"] }, // "FMTTYPE": { ... }, // See ALTREP "fbtype": { values: ["FREE", "BUSY", "BUSY-UNAVAILABLE", "BUSY-TENTATIVE"], allowXName: true, allowIanaToken: true }, // "LANGUAGE": { ... }, // See ALTREP "member": { valueType: "cal-address", multiValue: "," }, "partstat": { // TODO These values are actually different per-component values: ["NEEDS-ACTION", "ACCEPTED", "DECLINED", "TENTATIVE", "DELEGATED", "COMPLETED", "IN-PROCESS"], allowXName: true, allowIanaToken: true }, "range": { values: ["THISLANDFUTURE"] }, "related": { values: ["START", "END"] }, "reltype": { values: ["PARENT", "CHILD", "SIBLING"], allowXName: true, allowIanaToken: true }, "role": { values: ["REQ-PARTICIPANT", "CHAIR", "OPT-PARTICIPANT", "NON-PARTICIPANT"], allowXName: true, allowIanaToken: true }, "rsvp": { valueType: "boolean" }, "sent-by": { valueType: "cal-address" }, "tzid": { matches: /^\// }, "value": { // since the value here is a 'type' lowercase is used. values: ["binary", "boolean", "cal-address", "date", "date-time", "duration", "float", "integer", "period", "recur", "text", "time", "uri", "utc-offset"], allowXName: true, allowIanaToken: true } }, // When adding a value here, be sure to add it to the parameter types! value: { "binary": { decorate: function(aString) { return ICAL.icalbinary.fromString(aString); }, undecorate: function(aBinary) { return aBinary.toString(); } }, "boolean": { values: ["TRUE", "FALSE"], fromICAL: function(aValue) { switch(aValue) { case 'TRUE': return true; case 'FALSE': return false; default: //TODO: parser warning return false; } }, toICAL: function(aValue) { if (aValue) { return 'TRUE'; } return 'FALSE'; } }, "cal-address": { // needs to be an uri }, "date": { validate: function(aValue) { var state = { buffer: aValue }; var data = ICAL.DecorationParser.parseDate(state); ICAL.DecorationParser.expectEnd(state, "Junk at end of DATE value"); return data; }, decorate: function(aValue) { // YYYY-MM-DD // 2012-10-10 return new ICAL.icaltime({ year: strictParseInt(aValue.substr(0, 4)), month: strictParseInt(aValue.substr(5, 2)), day: strictParseInt(aValue.substr(8, 2)), isDate: true }); }, /** * undecorates a time object. */ undecorate: function(aValue) { // 2012-10-10 return aValue.year + '-' + ICAL.helpers.pad2(aValue.month) + '-' + ICAL.helpers.pad2(aValue.day); }, fromICAL: function(aValue) { // from: 20120901 // to: 2012-09-01 var result = aValue.substr(0, 4) + '-' + aValue.substr(4, 2) + '-' + aValue.substr(6, 2); return result; }, toICAL: function(aValue) { // from: 2012-09-01 // to: 20120901 if (aValue.length !== 10) { //TODO: serialize warning? return aValue; } return aValue.substr(0, 4) + aValue.substr(5, 2) + aValue.substr(8, 2); } }, "date-time": { validate: function(aValue) { var state = { buffer: aValue }; var data = ICAL.DecorationParser.parseDateTime(state); ICAL.DecorationParser.expectEnd(state, "Junk at end of DATE-TIME value"); return data; }, fromICAL: function(aValue) { // from: 20120901T130000 // to: 2012-09-01T13:00:00 var result = aValue.substr(0, 4) + '-' + aValue.substr(4, 2) + '-' + aValue.substr(6, 2) + 'T' + aValue.substr(9, 2) + ':' + aValue.substr(11, 2) + ':' + aValue.substr(13, 2); if (aValue[15] === 'Z') { result += 'Z' } return result; }, toICAL: function(aValue) { // from: 2012-09-01T13:00:00 // to: 20120901T130000 if (aValue.length < 19) { // TODO: error return aValue; } var result = aValue.substr(0, 4) + aValue.substr(5, 2) + // grab the (DDTHH) segment aValue.substr(8, 5) + // MM aValue.substr(14, 2) + // SS aValue.substr(17, 2); if (aValue[19] === 'Z') { result += 'Z'; } return result; }, decorate: function(aValue) { if (aValue.length < 19) { throw new DecorationError( 'invalid date-time value: "' + aValue + '"' ); } // 2012-10-10T10:10:10(Z)? var time = new ICAL.icaltime({ year: strictParseInt(aValue.substr(0, 4)), month: strictParseInt(aValue.substr(5, 2)), day: strictParseInt(aValue.substr(8, 2)), hour: strictParseInt(aValue.substr(11, 2)), minute: strictParseInt(aValue.substr(14, 2)), second: strictParseInt(aValue.substr(17, 2)) }); if (aValue[19] === 'Z') { time.zone = ICAL.icaltimezone.utc_timezone; } return time; }, undecorate: function(aValue) { var result = aValue.year + '-' + ICAL.helpers.pad2(aValue.month) + '-' + ICAL.helpers.pad2(aValue.day) + 'T' + ICAL.helpers.pad2(aValue.hour) + ':' + ICAL.helpers.pad2(aValue.minute) + ':' + ICAL.helpers.pad2(aValue.second); if (aValue.zone === ICAL.icaltimezone.utc_timezone) { result += 'Z'; } return result; } }, duration: { decorate: function(aValue) { return ICAL.icalduration.fromString(aValue); }, undecorate: function(aValue) { return aValue.toString(); } }, float: { matches: /^[+-]?\d+\.\d+$/, decorate: function(aValue) { return ICAL.icalvalue.fromString(aValue, "float"); }, fromICAL: function(aValue) { var parsed = parseFloat(aValue); if (isStrictlyNaN(parsed)) { // TODO: parser warning return 0.0; } return parsed; }, toICAL: function(aValue) { return String(aValue); } }, integer: { fromICAL: function(aValue) { var parsed = parseInt(aValue); if (isStrictlyNaN(parsed)) { return 0; } return parsed; }, toICAL: function(aValue) { return String(aValue); } }, period: { validate: function(aValue) { var state = { buffer: aValue }; var data = ICAL.DecorationParser.parsePeriod(state); ICAL.DecorationParser.expectEnd(state, "Junk at end of PERIOD value"); return data; }, decorate: function(aValue) { return ICAL.icalperiod.fromString(aValue); }, undecorate: function(aValue) { return aValue.toString(); } }, recur: { validate: function(aValue) { var state = { buffer: aValue }; var data = ICAL.DecorationParser.parseRecur(state); ICAL.DecorationParser.expectEnd(state, "Junk at end of RECUR value"); return data; }, decorate: function decorate(aValue) { return ICAL.icalrecur.fromString(aValue); }, undecorate: function(aRecur) { return aRecur.toString(); } }, text: { matches: /.*/, fromICAL: function(aValue, aName) { return replaceNewline(aValue); }, toICAL: function escape(aValue, aName) { return aValue.replace(/\\|;|,|\n/g, function(str) { switch (str) { case "\\": return "\\\\"; case ";": return "\\;"; case ",": return "\\,"; case "\n": return "\\n"; default: return str; } }); } }, time: { validate: function(aValue) { var state = { buffer: aValue }; var data = ICAL.DecorationParser.parseTime(state); ICAL.DecorationParser.expectEnd(state, "Junk at end of TIME value"); return data; }, fromICAL: function(aValue) { // from: MMHHSS(Z)? // to: HH:MM:SS(Z)? if (aValue.length < 6) { // TODO: parser exception? return aValue; } // HH::MM::SSZ? var result = aValue.substr(0, 2) + ':' + aValue.substr(2, 2) + ':' + aValue.substr(4, 2); if (aValue[6] === 'Z') { result += 'Z'; } return result; }, toICAL: function(aValue) { // from: HH:MM:SS(Z)? // to: MMHHSS(Z)? if (aValue.length < 8) { //TODO: error return aValue; } var result = aValue.substr(0, 2) + aValue.substr(3, 2) + aValue.substr(6, 2); if (aValue[8] === 'Z') { result += 'Z'; } return result; } }, uri: { // TODO /* ... */ }, "utc-offset": { validate: function(aValue) { var state = { buffer: aValue }; var data = ICAL.DecorationParser.parseUtcOffset(state); ICAL.DecorationParser.expectEnd(state, "Junk at end of UTC-OFFSET value"); return data; }, decorate: function(aValue) { return ICAL.icalutcoffset.fromString(aValue); }, undecorate: function(aValue) { return aValue.toString(); } } }, property: { decorate: function decorate(aData, aParent) { return new ICAL.Property(aData, aParent); }, "attach": { defaultType: "uri" }, "attendee": { defaultType: "cal-address" }, "categories": { defaultType: "text", multiValue: "," }, "completed": { defaultType: "date-time" }, "created": { defaultType: "date-time" }, "dtend": { defaultType: "date-time", allowedTypes: ["date-time", "date"] }, "dtstamp": { defaultType: "date-time" }, "dtstart": { defaultType: "date-time", allowedTypes: ["date-time", "date"] }, "due": { defaultType: "date-time", allowedTypes: ["date-time", "date"] }, "duration": { defaultType: "duration" }, "exdate": { defaultType: "date-time", allowedTypes: ["date-time", "date"], multiValue: ',' }, "exrule": { defaultType: "recur" }, "freebusy": { defaultType: "period", multiValue: "," }, "geo": { defaultType: "float", multiValue: ";" }, /* TODO exactly 2 values */"last-modified": { defaultType: "date-time" }, "organizer": { defaultType: "cal-address" }, "percent-complete": { defaultType: "integer" }, "repeat": { defaultType: "integer" }, "rdate": { defaultType: "date-time", allowedTypes: ["date-time", "date", "period"], multiValue: ',' }, "recurrence-id": { defaultType: "date-time", allowedTypes: ["date-time", "date"] }, "resources": { defaultType: "text", multiValue: "," }, "request-status": { defaultType: "text", multiValue: ";" }, "priority": { defaultType: "integer" }, "rrule": { defaultType: "recur" }, "sequence": { defaultType: "integer" }, "trigger": { defaultType: "duration", allowedTypes: ["duration", "date-time"] }, "tzoffsetfrom": { defaultType: "utc-offset" }, "tzoffsetto": { defaultType: "utc-offset" }, "tzurl": { defaultType: "uri" }, "url": { defaultType: "uri" } }, component: { decorate: function decorate(aData, aParent) { return new ICAL.Component(aData, aParent); }, "vevent": {} } }; return design; }()); ICAL.stringify = (function() { 'use strict'; var LINE_ENDING = '\r\n'; var DEFAULT_TYPE = 'text'; var design = ICAL.design; var helpers = ICAL.helpers; /** * Convert a full jCal Array into a ical document. * * @param {Array} jCal document. * @return {String} ical document. */ function stringify(jCal) { if (!jCal[0] || jCal[0] !== 'icalendar') { throw new Error('must provide full jCal document'); } // 1 because we skip the initial element. var i = 1; var len = jCal.length; var result = ''; for (; i < len; i++) { result += stringify.component(jCal[i]) + LINE_ENDING; } return result; } /** * Converts an jCal component array into a ICAL string. * Recursive will resolve sub-components. * * Exact component/property order is not saved all * properties will come before subcomponents. * * @param {Array} component jCal fragment of a component. */ stringify.component = function(component) { var name = component[0].toUpperCase(); var result = 'BEGIN:' + name + LINE_ENDING; var props = component[1]; var propIdx = 0; var propLen = props.length; for (; propIdx < propLen; propIdx++) { result += stringify.property(props[propIdx]) + LINE_ENDING; } var comps = component[2]; var compIdx = 0; var compLen = comps.length; for (; compIdx < compLen; compIdx++) { result += stringify.component(comps[compIdx]) + LINE_ENDING; } result += 'END:' + name; return result; } /** * Converts a single property to a ICAL string. * * @param {Array} property jCal property. */ stringify.property = function(property) { var name = property[0].toUpperCase(); var jsName = property[0]; var params = property[1]; var line = name; var paramName; for (paramName in params) { if (params.hasOwnProperty(paramName)) { line += ';' + paramName.toUpperCase(); line += '=' + stringify.propertyValue(params[paramName]); } } // there is no value so return. if (property.length === 3) { // if no params where inserted and no value // we given we must add a blank value. if (!paramName) { line += ':'; } return line; } var valueType = property[2]; var propDetails; var multiValue = false; var isDefault = false; if (jsName in design.property) { propDetails = design.property[jsName]; if ('multiValue' in propDetails) { multiValue = propDetails.multiValue; } if ('defaultType' in propDetails) { if (valueType === propDetails.defaultType) { isDefault = true; } } else { if (valueType === DEFAULT_TYPE) { isDefault = true; } } } else { if (valueType === DEFAULT_TYPE) { isDefault = true; } } // push the VALUE property if type is not the default // for the current property. if (!isDefault) { // value will never contain ;/:/, so we don't escape it here. line += ';VALUE=' + valueType.toUpperCase(); } line += ':'; if (multiValue) { line += stringify.multiValue( property.slice(3), multiValue, valueType ); } else { line += stringify.value(property[3], valueType); } return ICAL.helpers.foldline(line); } /** * Handles escaping of property values that may contain: * * COLON (:), SEMICOLON (;), or COMMA (,) * * If any of the above are present the result is wrapped * in double quotes. * * @param {String} value raw value. * @return {String} given or escaped value when needed. */ stringify.propertyValue = function(value) { if ((helpers.unescapedIndexOf(value, ',') === -1) && (helpers.unescapedIndexOf(value, ':') === -1) && (helpers.unescapedIndexOf(value, ';') === -1)) { return value; } return '"' + value + '"'; } /** * Converts an array of ical values into a single * string based on a type and a delimiter value (like ","). * * @param {Array} values list of values to convert. * @param {String} delim used to join the values usually (",", ";", ":"). * @param {String} type lowecase ical value type * (like boolean, date-time, etc..). * * @return {String} ical string for value. */ stringify.multiValue = function(values, delim, type) { var result = ''; var len = values.length; var i = 0; for (; i < len; i++) { result += stringify.value(values[i], type); if (i !== (len - 1)) { result += delim; } } return result; } /** * Processes a single ical value runs the associated "toICAL" * method from the design value type if available to convert * the value. * * @param {String|Numeric} value some formatted value. * @param {String} type lowecase ical value type * (like boolean, date-time, etc..). * @return {String} ical value for single value. */ stringify.value = function(value, type) { if (type in design.value && 'toICAL' in design.value[type]) { return design.value[type].toICAL(value); } return value; } return stringify; }()); ICAL.parse = (function() { 'use strict'; var CHAR = /[^ \t]/; var MULTIVALUE_DELIMITER = ','; var VALUE_DELIMITER = ':'; var PARAM_DELIMITER = ';'; var PARAM_NAME_DELIMITER = '='; var DEFAULT_TYPE = 'text'; var design = ICAL.design; var helpers = ICAL.helpers; function ParserError(message) { Error.apply(this, arguments); } ParserError.prototype = { __proto__: Error.prototype }; function parser(input) { var state = {}; var root = state.component = [ 'icalendar' ]; state.stack = [root]; parser._eachLine(input, function(err, line) { parser._handleContentLine(line, state); }); // when there are still items on the stack // throw a fatal error, a component was not closed // correctly in that case. if (state.stack.length > 1) { throw new ParserError( 'invalid ical body, a began started but did not end' ); } state = null; return root; } // classes & constants parser.ParserError = ParserError; parser._formatName = function(name) { return name.toLowerCase(); } parser._handleContentLine = function(line, state) { // break up the parts of the line var valuePos = line.indexOf(VALUE_DELIMITER); var paramPos = line.indexOf(PARAM_DELIMITER); var nextPos = 0; // name of property or begin/end var name; var value; var params; /** * Different property cases * * * 1. RRULE:FREQ=foo * // FREQ= is not a param but the value * * 2. ATTENDEE;ROLE=REQ-PARTICIPANT; * // ROLE= is a param because : has not happened yet */ if ((paramPos !== -1 && valuePos !== -1)) { // when the parameter delimiter is after the // value delimiter then its not a parameter. if (paramPos > valuePos) { paramPos = -1; } } if (paramPos !== -1) { // when there are parameters (ATTENDEE;RSVP=TRUE;) name = parser._formatName(line.substr(0, paramPos)); params = parser._parseParameters(line, paramPos); if (valuePos !== -1) { value = line.substr(valuePos + 1); } } else if (valuePos !== -1) { // without parmeters (BEGIN:VCAENDAR, CLASS:PUBLIC) name = parser._formatName(line.substr(0, valuePos)); value = line.substr(valuePos + 1); if (name === 'begin') { var newComponent = [parser._formatName(value), [], []]; if (state.stack.length === 1) { state.component.push(newComponent); } else { state.component[2].push(newComponent); } state.stack.push(state.component); state.component = newComponent; return; } if (name === 'end') { state.component = state.stack.pop(); return; } } else { /** * Invalid line. * The rational to throw an error is we will * never be certain that the rest of the file * is sane and its unlikely that we can serialize * the result correctly either. */ throw new ParserError( 'invalid line (no token ";" or ":") "' + line + '"' ); } var valueType; var multiValue = false; var propertyDetails; if (name in design.property) { propertyDetails = design.property[name]; if ('multiValue' in propertyDetails) { multiValue = propertyDetails.multiValue; } } // at this point params is mandatory per jcal spec params = params || {}; // attempt to determine value if (!('value' in params)) { if (propertyDetails) { valueType = propertyDetails.defaultType; } else { valueType = DEFAULT_TYPE; } } else { // possible to avoid this? valueType = params.value.toLowerCase(); delete params.value; } /** * Note on `var result` juggling: * * I observed that building the array in pieces has adverse * effects on performance, so where possible we inline the creation. * Its a little ugly but resulted in ~2000 additional ops/sec. */ if (value) { if (multiValue) { var result = [name, params, valueType]; parser._parseMultiValue(value, multiValue, valueType, result); } else { value = parser._parseValue(value, valueType); var result = [name, params, valueType, value]; } } else { var result = [name, params, valueType]; } state.component[1].push(result); }; /** * @param {String} value original value. * @param {String} type type of value. * @return {Object} varies on type. */ parser._parseValue = function(value, type) { if (type in design.value && 'fromICAL' in design.value[type]) { return design.value[type].fromICAL(value); } return value; }; /** * Parse parameters from a string to object. * * @param {String} line a single unfolded line. * @param {Numeric} start position to start looking for properties. * @param {Numeric} maxPos position at which values start. * @return {Object} key/value pairs. */ parser._parseParameters = function(line, start) { var lastParam = start; var pos = 0; var delim = PARAM_NAME_DELIMITER; var result = {}; // find the next '=' sign // use lastParam and pos to find name // check if " is used if so get value from "->" // then increment pos to find next ; while ((pos !== false) && (pos = helpers.unescapedIndexOf(line, delim, pos + 1)) !== -1) { var name = line.substr(lastParam + 1, pos - lastParam - 1); var nextChar = line[pos + 1]; var substrOffset = -2; if (nextChar === '"') { var valuePos = pos + 2; pos = helpers.unescapedIndexOf(line, '"', valuePos); var value = line.substr(valuePos, pos - valuePos); lastParam = helpers.unescapedIndexOf(line, PARAM_DELIMITER, pos); } else { var valuePos = pos + 1; substrOffset = -1; // move to next ";" var nextPos = helpers.unescapedIndexOf(line, PARAM_DELIMITER, valuePos); if (nextPos === -1) { // when there is no ";" attempt to locate ":" nextPos = helpers.unescapedIndexOf(line, VALUE_DELIMITER, valuePos); // no more tokens end of the line use .length if (nextPos === -1) { nextPos = line.length; // because we are at the end we don't need to trim // the found value of substr offset is zero substrOffset = 0; } else { // next token is the beginning of the value // so we must stop looking for the '=' token. pos = false; } } else { lastParam = nextPos; } var value = line.substr(valuePos, nextPos - valuePos); } var type = DEFAULT_TYPE; if (name in design.param && design.param[name].valueType) { type = design.param[name].valueType; } result[parser._formatName(name)] = parser._parseValue(value, type); } return result; } /** * Parse a multi value string */ parser._parseMultiValue = function(buffer, delim, type, result) { var pos = 0; var lastPos = 0; // split each piece while ((pos = helpers.unescapedIndexOf(buffer, delim, lastPos)) !== -1) { var value = buffer.substr(lastPos, pos - lastPos); result.push(parser._parseValue(value, type)); lastPos = pos + 1; } // on the last piece take the rest of string result.push( parser._parseValue(buffer.substr(lastPos), type) ); return result; } parser._eachLine = function(buffer, callback) { var len = buffer.length; var lastPos = buffer.search(CHAR); var pos = lastPos; var line; var firstChar; var newlineOffset; do { pos = buffer.indexOf('\n', lastPos) + 1; if (buffer[pos - 2] === '\r') { newlineOffset = 2; } else { newlineOffset = 1; } if (pos === 0) { pos = len; newlineOffset = 0; } firstChar = buffer[lastPos]; if (firstChar === ' ' || firstChar === '\t') { // add to line line += buffer.substr( lastPos + 1, pos - lastPos - (newlineOffset + 1) ); } else { if (line) callback(null, line); // push line line = buffer.substr( lastPos, pos - lastPos - newlineOffset ); } lastPos = pos; } while (pos !== len); // extra ending line line = line.trim(); if (line.length) callback(null, line); } return parser; }()); /* This Source Code Form is subject to the terms of the Mozilla Public console.log() * 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() { /** * home of the original parser, we have re-purposed most * of the decoration parsing here in the icaltype classes (like period). * Eventually we need to to through each of these and make sure * they are as efficient as possible. Most of these are very complete * and validate input but are slow due to their heavy use of RegExp. */ 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 = { Error: ParserError }; ICAL.DecorationParser = parser; 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] != 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.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: parseInt(parser.expectRE(aState, /^((\d+)W)/, "Expected Weeks")[2], 10) }; } 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 = parseInt(days[2], 10); } 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); } } })(); ICAL.Component = (function() { 'use strict'; var PROPERTY_INDEX = 1; var COMPONENT_INDEX = 2; var NAME_INDEX = 0; /** * Create a wrapper for a jCal component. * * @param {Array|String} jCal * raw jCal component data OR name of new component. * @param {ICAL.Component} parent parent component to associate. */ function Component(jCal, parent) { if (typeof(jCal) === 'string') { // jCal spec (name, properties, components) jCal = [jCal, [], []]; } // mostly for legacy reasons. this.jCal = jCal; if (parent) { this.parent = parent; } } Component.prototype = { get name() { return this.jCal[NAME_INDEX]; }, _hydrateComponent: function(index) { if (!this._components) { this._components = []; } if (this._components[index]) { return this._components[index]; } var comp = new Component( this.jCal[COMPONENT_INDEX][index], this ); return this._components[index] = comp; }, _hydrateProperty: function(index) { if (!this._properties) { this._properties = []; } if (this._properties[index]) { return this._properties[index]; } var prop = new ICAL.Property( this.jCal[PROPERTY_INDEX][index], this ); return this._properties[index] = prop; }, /** * Finds first sub component, optionally filtered by name. * * @method getFirstSubcomponent * @param {String} [name] optional name to filter by. */ getFirstSubcomponent: function(name) { if (name) { var i = 0; var comps = this.jCal[COMPONENT_INDEX]; var len = comps.length; for (; i < len; i++) { if (comps[i][NAME_INDEX] === name) { var result = this._hydrateComponent(i); return result; } } } else { if (this.jCal[COMPONENT_INDEX].length) { return this._hydrateComponent(0); } } // ensure we return a value (strict mode) return null; }, /** * Finds all sub components, optionally filtering by name. * * @method getAllSubcomponents * @param {String} [name] optional name to filter by. */ getAllSubcomponents: function(name) { var jCalLen = this.jCal[COMPONENT_INDEX].length; if (name) { var comps = this.jCal[COMPONENT_INDEX]; var result = []; var i = 0; for (; i < jCalLen; i++) { if (name === comps[i][NAME_INDEX]) { result.push( this._hydrateComponent(i) ); } } return result; } else { if (!this._components || (this._components.length !== jCalLen)) { var i = 0; for (; i < jCalLen; i++) { this._hydrateComponent(i); } } return this._components; } }, /** * Returns true when a named property exists. * * @param {String} name property name. * @return {Boolean} true when property is found. */ hasProperty: function(name) { var props = this.jCal[PROPERTY_INDEX]; var len = props.length; var i = 0; for (; i < len; i++) { // 0 is property name if (props[i][NAME_INDEX] === name) { return true; } } return false; }, /** * Finds first property. * * @param {String} [name] lowercase name of property. * @return {ICAL.Property} found property. */ getFirstProperty: function(name) { if (name) { var i = 0; var props = this.jCal[PROPERTY_INDEX]; var len = props.length; for (; i < len; i++) { if (props[i][NAME_INDEX] === name) { var result = this._hydrateProperty(i); return result; } } } else { if (this.jCal[PROPERTY_INDEX].length) { return this._hydrateProperty(0); } } return null; }, /** * Returns first properties value if available. * * @param {String} [name] (lowecase) property name. * @return {String} property value. */ getFirstPropertyValue: function(name) { var prop = this.getFirstProperty(name); if (prop) { return prop.getFirstValue(); } return null; }, /** * get all properties in the component. * * @param {String} [name] (lowercase) property name. * @return {Array[ICAL.Property]} list of properties. */ getAllProperties: function(name) { var jCalLen = this.jCal[PROPERTY_INDEX].length; if (name) { var props = this.jCal[PROPERTY_INDEX]; var result = []; var i = 0; for (; i < jCalLen; i++) { if (name === props[i][NAME_INDEX]) { result.push( this._hydrateProperty(i) ); } } return result; } else { if (!this._properties || (this._properties.length !== jCalLen)) { var i = 0; for (; i < jCalLen; i++) { this._hydrateProperty(i); } } return this._properties; } return null; }, _removeObjectByIndex: function(jCalIndex, cache, index) { // remove cached version if (cache && cache[index]) { cache.splice(index, 1); } // remove it from the jCal this.jCal[jCalIndex].splice(index, 1); }, _removeObject: function(jCalIndex, cache, nameOrObject) { var i = 0; var objects = this.jCal[jCalIndex]; var len = objects.length; var cached = this[cache]; if (typeof(nameOrObject) === 'string') { for (; i < len; i++) { if (objects[i][NAME_INDEX] === nameOrObject) { this._removeObjectByIndex(jCalIndex, cached, i); return true; } } } else if (cached) { for (; i < len; i++) { if (cached[i] && cached[i] === nameOrObject) { this._removeObjectByIndex(jCalIndex, cached, i); return true; } } } return false; }, _removeAllObjects: function(jCalIndex, cache, name) { var cached = this[cache]; if (name) { var objects = this.jCal[jCalIndex]; var i = objects.length - 1; // descending search required because splice // is used and will effect the indices. for (; i >= 0; i--) { if (objects[i][NAME_INDEX] === name) { this._removeObjectByIndex(jCalIndex, cached, i); } } } else { if (cache in this) { // I think its probable that when we remove all // of a type we may want to add to it again so it // makes sense to reuse the object in that case. // For now we remove the contents of the array. this[cache].length = 0; } this.jCal[jCalIndex].length = 0; } }, /** * Adds a single sub component. * * @param {ICAL.Component} component to add. */ addSubcomponent: function(component) { if (!this._components) { this._components = []; } var idx = this.jCal[COMPONENT_INDEX].push(component.jCal); this._components[idx - 1] = component; }, /** * Removes a single component by name or * the instance of a specific component. * * @param {ICAL.Component|String} nameOrComp comp type. * @return {Boolean} true when comp is removed. */ removeSubcomponent: function(nameOrComp) { return this._removeObject(COMPONENT_INDEX, '_components', nameOrComp); }, /** * Removes all components or (if given) all * components by a particular name. * * @param {String} [name] (lowercase) component name. */ removeAllSubcomponents: function(name) { return this._removeAllObjects(COMPONENT_INDEX, '_components', name); }, /** * Adds a property to the component. * * @param {ICAL.Property} property object. */ addProperty: function(property) { if (!(property instanceof ICAL.Property)) { throw new TypeError('must instance of ICAL.Property'); } var idx = this.jCal[PROPERTY_INDEX].push(property.jCal); property.component = this; if (!this._properties) { this._properties = []; } this._properties[idx - 1] = property; }, /** * Helper method to add a property with a value to the component. * * @param {String} name property name to add. * @param {Object} value property value. */ addPropertyWithValue: function(name, value) { var prop = new ICAL.Property(name, this); prop.setValue(value); this.addProperty(prop, this); return prop; }, /** * Helper method that will update or create a property * of the given name and sets its value. * * @param {String} name property name. * @param {Object} value property value. * @return {ICAL.Property} property. */ updatePropertyWithValue: function(name, value) { var prop = this.getFirstProperty(name); if (prop) { prop.setValue(value); } else { prop = this.addPropertyWithValue(name, value); } return prop; }, /** * Removes a single property by name or * the instance of the specific property. * * @param {String|ICAL.Property} nameOrProp to remove. * @return {Boolean} true when deleted. */ removeProperty: function(nameOrProp) { return this._removeObject(PROPERTY_INDEX, '_properties', nameOrProp); }, /** * Removes all properties associated with this component. * * @param {String} [name] (lowecase) optional property name. */ removeAllProperties: function(name) { return this._removeAllObjects(PROPERTY_INDEX, '_properties', name); }, toJSON: function() { return this.jCal; }, toString: function() { return ICAL.stringify.component( this.jCal ); } }; return Component; }()); ICAL.Property = (function() { 'use strict'; var NAME_INDEX = 0; var PROP_INDEX = 1; var TYPE_INDEX = 2; var VALUE_INDEX = 3; var design = ICAL.design; /** * Provides a nicer interface to any kind of property. * Its important to note that mutations done in the wrapper * directly effect (mutate) the jCal object used to initialize. * * Can also be used to create new properties by passing * the name of the property (as a String). * * * @param {Array|String} jCal raw jCal representation OR * the new name of the property (when creating). * * @param {ICAL.Component} [component] parent component. */ function Property(jCal, component) { if (typeof(jCal) === 'string') { // because we a creating by name we need // to find the type when creating the property. var name = jCal; if (name in design.property) { var prop = design.property[name]; if ('defaultType' in prop) { var type = prop.defaultType; } else { var type = design.defaultType; } } else { var type = design.defaultType; } jCal = [name, {}, type]; } this.jCal = jCal; this.component = component; this._updateType(); } Property.prototype = { get type() { return this.jCal[TYPE_INDEX]; }, get name() { return this.jCal[NAME_INDEX]; }, _updateType: function() { if (this.type in design.value) { var designType = design.value[this.type]; if ('decorate' in design.value[this.type]) { this.isDecorated = true; } else { this.isDecorated = false; } if (this.name in design.property) { if ('multiValue' in design.property[this.name]) { this.isMultiValue = true; } else { this.isMultiValue = false; } } } }, /** * Hydrate a single value. */ _hydrateValue: function(index) { if (this._values && this._values[index]) { return this._values[index]; } // for the case where there is no value. if (this.jCal.length <= (VALUE_INDEX + index)) { return null; } if (this.isDecorated) { if (!this._values) { this._values = []; } return this._values[index] = this._decorate( this.jCal[VALUE_INDEX + index] ); } else { return this.jCal[VALUE_INDEX + index]; } }, _decorate: function(value) { return design.value[this.type].decorate(value); }, _undecorate: function(value) { return design.value[this.type].undecorate(value); }, _setDecoratedValue: function(value, index) { if (!this._values) { this._values = []; } if (typeof(value) === 'object' && 'icaltype' in value) { // decorated value this.jCal[VALUE_INDEX + index] = this._undecorate(value); this._values[index] = value; } else { // undecorated value this.jCal[VALUE_INDEX + index] = value; this._values[index] = this._decorate(value); } }, /** * Gets a param on the property. * * @param {String} name prop name (lowercase). * @return {String} prop value. */ getParameter: function(name) { return this.jCal[PROP_INDEX][name]; }, /** * Sets a param on the property. * * @param {String} value property value. */ setParameter: function(name, value) { this.jCal[PROP_INDEX][name] = value; }, /** * Removes a parameter * * @param {String} name prop name (lowercase). */ removeParameter: function(name) { return delete this.jCal[PROP_INDEX][name]; }, /** * Sets type of property and clears out any * existing values of the current type. * * @param {String} type new iCAL type (see design.values). */ resetType: function(type) { this.removeAllValues(); this.jCal[TYPE_INDEX] = type; this._updateType(); }, /** * Finds first property value. * * @return {String} first property value. */ getFirstValue: function() { return this._hydrateValue(0); }, /** * Gets all values on the property. * * NOTE: this creates an array during each call. * * @return {Array} list of values. */ getValues: function() { var len = this.jCal.length - VALUE_INDEX; if (len < 1) { // its possible for a property to have no value. return; } var i = 0; var result = []; for (; i < len; i++) { result[i] = this._hydrateValue(i); } return result; }, removeAllValues: function() { if (this._values) { this._values.length = 0; } this.jCal.length = 3; }, /** * Sets the values of the property. * Will overwrite the existing values. * * @param {Array} values an array of values. */ setValues: function(values) { if (!this.isMultiValue) { throw new Error( this.name + ': does not not support mulitValue.\n' + 'override isMultiValue' ); } var len = values.length; var i = 0; this.removeAllValues(); if (this.isDecorated) { for (; i < len; i++) { this._setDecoratedValue(values[i], i); } } else { for (; i < len; i++) { this.jCal[VALUE_INDEX + i] = values[i]; } } }, /** * Sets the current value of the property. * * @param {String|Object} value new prop value. */ setValue: function(value) { if (this.isDecorated) { this._setDecoratedValue(value, 0); } else { this.jCal[VALUE_INDEX] = value; } }, /** * Returns the jCal representation of this property. * * @return {Object} jCal. */ toJSON: function() { return this.jCal; }, toICAL: function() { return ICAL.stringify.property( this.jCal ); } }; return Property; }()); /* 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.DecorationParser.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.DecorationParser.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() { var DURATION_LETTERS = /([PDWHMTS]{1,1})/; 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(); }; /** * Internal helper function to handle a chunk of a duration. * * @param {String} letter type of duration chunk. * @param {String} number numeric value or -/+. * @param {Object} dict target to assign values to. */ function parseDurationChunk(letter, number, object) { var type; switch (letter) { case 'P': if (number && number === '-') { object.isNegative = true; } else { object.isNegative = false; } // period break; case 'D': type = 'days'; break; case 'W': type = 'weeks'; break; case 'H': type = 'hours'; break; case 'M': type = 'minutes'; break; case 'S': type = 'seconds'; break; } if (type) { object[type] = parseInt(number); } } ICAL.icalduration.fromString = function icalduration_from_string(aStr) { var pos = 0; var dict = Object.create(null); while ((pos = aStr.search(DURATION_LETTERS)) !== -1) { var type = aStr[pos]; var numeric = aStr.substr(0, pos); aStr = aStr.substr(pos + 1); parseDurationChunk(type, numeric, dict); } return new ICAL.icalduration(dict); }; 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.DecorationParser.parseValue(str, "date"); data.isDate = true; } catch (e) { data = ICAL.DecorationParser.parseValue(str, "date-time"); data.isDate = false; } return this.fromData(data); }, fromJSDate: function icaltime_fromJSDate(aDate, useUTC) { if (!aDate) { this.reset(); } else { if (useUTC) { this.zone = ICAL.icaltimezone.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) { var timezone = aData.timezone; //TODO: replace with timezone service switch (timezone) { case 'Z': case ICAL.icaltimezone.utc_timezone.tzid: this.zone = ICAL.icaltimezone.utc_timezone; break; case ICAL.icaltimezone.local_timezone.tzid: this.zone = ICAL.icaltimezone.local_timezone; break; } } 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; }, dayOfWeek: function icaltime_dayOfWeek() { // Using Zeller's algorithm var q = this.day; var m = this.month + (this.month < 3 ? 12 : 0); var Y = this.year - (this.month < 3 ? 1 : 0); var h = (q + Y + ICAL.helpers.trunc(((m + 1) * 26) / 10) + ICAL.helpers.trunc(Y / 4)); if (true /* gregorian */) { h += ICAL.helpers.trunc(Y / 100) * 6 + ICAL.helpers.trunc(Y / 400); } else { h += 5; } // Normalize to 1 = sunday h = ((h + 6) % 7) + 1; return h; }, dayOfYear: function icaltime_dayOfYear() { var is_leap = (ICAL.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; }, startOfWeek: function startOfWeek() { var result = this.clone(); result.day -= this.dayOfWeek() - 1; return result.normalize(); }, end_of_week: function end_of_week() { var result = this.clone(); result.day += 7 - this.dayOfWeek(); 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.daysInMonth(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.dayOfWeek() - firstDow; if (delta < 0) delta += 7; return this.dayOfYear() - delta; }, /** * Finds the nthWeekDay relative to the current month (not day). * The returned value is a day relative the month that this * month belongs to so 1 would indicate the first of the month * and 40 would indicate a day in the following month. * * @param {Numeric} aDayOfWeek day of the week see the day name constants. * @param {Numeric} aPos nth occurrence of a given week day * values of 1 and 0 both indicate the first * weekday of that type. aPos may be either positive * or negative. * * @return {Numeric} numeric value indicating a day relative * to the current month of this time object. */ nthWeekDay: function icaltime_nthWeekDay(aDayOfWeek, aPos) { var daysInMonth = ICAL.icaltime.daysInMonth(this.month, this.year); var weekday; var pos = aPos; var start = 0; var otherDay = this.clone(); if (pos >= 0) { otherDay.day = 1; // because 0 means no position has been given // 1 and 0 indicate the same day. if (pos != 0) { // remove the extra numeric value pos--; } // set current start offset to current day. start = otherDay.day; // find the current day of week var startDow = otherDay.dayOfWeek(); // calculate the difference between current // day of the week and desired day of the week var offset = aDayOfWeek - startDow; // if the offset goes into the past // week we add 7 so its goes into the next // week. We only want to go forward in time here. if (offset < 0) // this is really important otherwise we would // end up with dates from in the past. offset += 7; // add offset to start so start is the same // day of the week as the desired day of week. start += offset; // because we are going to add (and multiply) // the numeric value of the day we subtract it // from the start position so not to add it twice. start -= aDayOfWeek; // set week day weekday = aDayOfWeek; } else { // then we set it to the last day in the current month otherDay.day = daysInMonth; // find the ends weekday var endDow = otherDay.dayOfWeek(); pos++; weekday = (endDow - aDayOfWeek); if (weekday < 0) { weekday += 7; } weekday = daysInMonth - weekday; } weekday += pos * 7; return start + weekday; }, /** * Checks if current time is the nthWeekDay. * Relative to the current month. * * Will always return false when rule resolves * outside of current month. * * @param {Numeric} aDayOfWeek day of week. * @param {Numeric} aPos position. * @param {Numeric} aMax maximum valid day. */ isNthWeekDay: function(aDayOfWeek, aPos) { var dow = this.dayOfWeek(); if (aPos === 0 && dow === aDayOfWeek) { return true; } // get pos var day = this.nthWeekDay(aDayOfWeek, aPos); if (day === this.day) { return true; } return false; }, 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.dayOfYear(); var dow = this.dayOfWeek(); var year = this.year; var week1; var dt = this.clone(); dt.isDate = true; var first_dow = dt.dayOfWeek(); var isoyear = this.year; if (dt.month == 12 && dt.day > 28) { week1 = ICAL.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.dayOfYear(); var that_doy = aDate.dayOfYear(); 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.dayOfYear(); dur.days -= days_left_thisyear + aDate.dayOfYear(); 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.dayOfYear(); dur.days += days_left_thatyear + this.dayOfYear(); 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 daysInMonth; 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 daysInMonth = ICAL.icaltime.daysInMonth(this.month, this.year); if (day <= daysInMonth) { break; } this.month++; if (this.month > 12) { this.year++; this.month = 1; } day -= daysInMonth; } } else { while (day <= 0) { if (this.month == 1) { this.year--; this.month = 12; } else { this.month--; } day += ICAL.icaltime.daysInMonth(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(); }, /** * Converts time to into Object * which can be serialized then re-created * using the constructor. * * Example: * * // toJSON will automatically be called * var json = JSON.stringify(mytime); * * var deserialized = JSON.parse(json); * * var time = new ICAL.icaltime(deserialized); * */ toJSON: function() { var copy = [ 'year', 'month', 'day', 'hour', 'minute', 'second', 'isDate' ]; var result = Object.create(null); var i = 0; var len = copy.length; var prop; for (; i < len; i++) { prop = copy[i]; result[prop] = this[prop]; } if (this.zone) { result.timezone = this.zone.tzid; } return result; } }; (function setupNormalizeAttributes() { // This needs to run before any instances are created! function 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.daysInMonth = function icaltime_daysInMonth(month, year) { var _daysInMonth = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; var days = 30; if (month < 1 || month > 12) return days; days = _daysInMonth[month]; if (month == 2) { days += ICAL.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.fromDayOfYear = function icaltime_fromDayOfYear(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.fromStringv2 = function fromString(str) { return new ICAL.icaltime({ year: parseInt(str.substr(0, 4), 10), month: parseInt(str.substr(5, 2), 10), day: parseInt(str.substr(8, 2), 10), isDate: true }); }; 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.dayOfWeek(); 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; ICAL.icaltime.DEFAULT_WEEK_START = ICAL.icaltime.MONDAY; })(); /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. * Portions Copyright (C) Philipp Kewisch, 2011-2012 */ (typeof(ICAL) === 'undefined')? ICAL = {} : ''; (function() { var DOW_MAP = { SU: ICAL.icaltime.SUNDAY, MO: ICAL.icaltime.MONDAY, TU: ICAL.icaltime.TUESDAY, WE: ICAL.icaltime.WEDNESDAY, TH: ICAL.icaltime.THURSDAY, FR: ICAL.icaltime.FRIDAY, SA: ICAL.icaltime.SATURDAY }; var REVERSE_DOW_MAP = {}; for (var key in DOW_MAP) { REVERSE_DOW_MAP[DOW_MAP[key]] = key; } 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 ICAL.icalrecur_iterator({ rule: this, dtstart: aStart }); }, clone: function clone() { return ICAL.icalrecur.fromData(this); //return ICAL.icalrecur.fromIcalProperty(this.toIcalProperty()); }, isFinite: function isfinite() { return !!(this.count || this.until); }, isByCount: function isbycount() { return !!(this.count && !this.until); }, addComponent: function addPart(aType, aValue) { if (!(aType in this.parts)) { this.parts[aType] = [aValue]; } else { this.parts[aType].push(aValue); } }, setComponent: function setComponent(aType, aValues) { this.parts[aType] = aValues; }, getComponent: function getComponent(aType, aCount) { var ucName = aType.toUpperCase(); var components = (ucName in this.parts ? this.parts[ucName] : []); if (aCount) aCount.value = components.length; return components; }, getNextOccurrence: function getNextOccurrence(aStartTime, aRecurrenceId) { 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; }, toJSON: function() { //XXX: extract this list up to proto? var propsToCopy = [ "freq", "count", "until", "wkst", "interval", "parts" ]; var result = Object.create(null); var i = 0; var len = propsToCopy.length; var prop; for (; i < len; i++) { var prop = propsToCopy[i]; result[prop] = this[prop]; } if (result.until instanceof ICAL.icaltime) { result.until = result.until.toJSON(); } return result; }, fromData: function fromData(aData) { var propsToCopy = ["freq", "count", "until", "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! } } // wkst is usually in SU, etc.. format we need // to convert it from the string if (typeof(this.wkst) === 'string') { this.wkst = ICAL.icalrecur.icalDayToNumericDay(this.wkst); } // Another hack for multiple construction of until value. if (this.until) { if (this.until instanceof ICAL.icaltime) { this.until = this.until.clone(); } else { this.until = ICAL.icaltime.fromData(this.until); } } 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]; } if (this.until ){ str += ';UNTIL=' + this.until.toString(); } if ('wkst' in this && this.wkst !== ICAL.icaltime.DEFAULT_WEEK_START) { str += ';WKST=' + REVERSE_DOW_MAP[this.wkst]; } return str; }, toIcalProperty: function toIcalProperty() { try { var valueData = { name: this.isNegative ? "EXRULE" : "RRULE", type: "RECUR", value: [this.toString()] // TODO more props? }; return ICAL.Property.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.DecorationParser.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; }; /** * Convert an ical representation of a day (SU, MO, etc..) * into a numeric value of that day. * * @param {String} day ical day. * @return {Numeric} numeric value of given day. */ ICAL.icalrecur.icalDayToNumericDay = function toNumericDay(string) { //XXX: this is here so we can deal // with possibly invalid string values. return DOW_MAP[string]; }; })(); ICAL.icalrecur_iterator = (function() { /** * Options: * - rule: (ICAL.icalrecur) instance * - dtstart: (ICAL.icaltime) start date of recurrence rule * - initialized: (Boolean) when true will assume options * are from previously constructed * iterator and will not re-initialize * iterator but resume its state from given data. * * - by_data: (for iterator de-serialization) * - days: " * - last: " * - by_indices: " */ function icalrecur_iterator(options) { this.fromData(options); } icalrecur_iterator.prototype = { /** * True when iteration is finished. */ completed: false, rule: null, dtstart: null, last: null, occurrence_number: 0, by_indices: null, initialized: false, by_data: null, days: null, days_index: 0, fromData: function(options) { this.rule = ICAL.helpers.formatClassType(options.rule, ICAL.icalrecur); if (!this.rule) { throw new Error('iterator requires a (ICAL.icalrecur) rule'); } this.dtstart = ICAL.helpers.formatClassType(options.dtstart, ICAL.icaltime); if (!this.dtstart) { throw new Error('iterator requires a (ICAL.icaltime) dtstart'); } if (options.by_data) { this.by_data = options.by_data; } else { this.by_data = ICAL.helpers.clone(this.rule.parts, true); } if (options.occurrence_number) this.occurrence_number = options.occurrence_number; this.days = options.days || []; this.last = ICAL.helpers.formatClassType(options.last, ICAL.icaltime); this.by_indices = options.by_indices; if (!this.by_indices) { this.by_indices = { "BYSECOND": 0, "BYMINUTE": 0, "BYHOUR": 0, "BYDAY": 0, "BYMONTH": 0, "BYWEEKNO": 0, "BYMONTHDAY": 0 }; } this.initialized = options.initialized || false; if (!this.initialized) { this.init(); } }, init: function icalrecur_iterator_init() { this.initialized = true; this.last = this.dtstart.clone(); var parts = this.by_data; if ("BYDAY" in parts) { // libical does this earlier when the rule is loaded, but we postpone to // now so we can preserve the original order. this.sort_byday_rules(parts.BYDAY, this.rule.wkst); } // If the BYYEARDAY appares, no other date rule part may appear if ("BYYEARDAY" in parts) { if ("BYMONTH" in parts || "BYWEEKNO" in parts || "BYMONTHDAY" in parts || "BYDAY" in parts) { throw new Error("Invalid BYYEARDAY rule"); } } // BYWEEKNO and BYMONTHDAY rule parts may not both appear if ("BYWEEKNO" in parts && "BYMONTHDAY" in parts) { throw new Error("BYWEEKNO does not fit to BYMONTHDAY"); } // For MONTHLY recurrences (FREQ=MONTHLY) neither BYYEARDAY nor // BYWEEKNO may appear. if (this.rule.freq == "MONTHLY" && ("BYYEARDAY" in parts || "BYWEEKNO" in parts)) { throw new Error("For MONTHLY recurrences neither BYYEARDAY nor BYWEEKNO may appear"); } // For WEEKLY recurrences (FREQ=WEEKLY) neither BYMONTHDAY nor // BYYEARDAY may appear. if (this.rule.freq == "WEEKLY" && ("BYYEARDAY" in parts || "BYMONTHDAY" in parts)) { throw new Error("For WEEKLY recurrences neither BYMONTHDAY nor BYYEARDAY may appear"); } // BYYEARDAY may only appear in YEARLY rules if (this.rule.freq != "YEARLY" && "BYYEARDAY" in parts) { throw new Error("BYYEARDAY may only appear in YEARLY rules"); } this.last.second = this.setup_defaults("BYSECOND", "SECONDLY", this.dtstart.second); this.last.minute = this.setup_defaults("BYMINUTE", "MINUTELY", this.dtstart.minute); this.last.hour = this.setup_defaults("BYHOUR", "HOURLY", this.dtstart.hour); this.last.day = this.setup_defaults("BYMONTHDAY", "DAILY", this.dtstart.day); this.last.month = this.setup_defaults("BYMONTH", "MONTHLY", this.dtstart.month); if (this.rule.freq == "WEEKLY") { if ("BYDAY" in parts) { var parts = this.ruleDayOfWeek(parts.BYDAY[0]); var pos = parts[0]; var rule_dow = parts[1]; var dow = rule_dow - this.last.dayOfWeek(); if ((this.last.dayOfWeek() < rule_dow && dow >= 0) || dow < 0) { // Initial time is after first day of BYDAY data this.last.day += dow; this.last.normalize(); } } else { var wkMap = icalrecur_iterator._wkdayMap[this.dtstart.dayOfWeek()]; 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.fromDayOfYear(this.days[0], this.last.year); this.last.day = next.day; this.last.month = next.month; } if (this.rule.freq == "MONTHLY" && this.has_by_data("BYDAY")) { var coded_day = this.by_data.BYDAY[this.by_indices.BYDAY]; var parts = this.ruleDayOfWeek(coded_day); var pos = parts[0]; var dow = parts[1]; var daysInMonth = ICAL.icaltime.daysInMonth(this.last.month, this.last.year); var poscount = 0; if (pos >= 0) { for (this.last.day = 1; this.last.day <= daysInMonth; this.last.day++) { if (this.last.dayOfWeek() == dow) { if (++poscount == pos || pos == 0) { break; } } } } else { pos = -pos; for (this.last.day = daysInMonth; this.last.day != 0; this.last.day--) { if (this.last.dayOfWeek() == dow) { if (++poscount == pos) { break; } } } } //XXX: This feels like a hack, but we need to initialize // the BYMONTHDAY case correctly and byDayAndMonthDay handles // this case. It accepts a special flag which will avoid incrementing // the initial value without the flag days that match the start time // would be missed. if (this.has_by_data('BYMONTHDAY')) { this._byDayAndMonthDay(true); } if (this.last.day > daysInMonth || this.last.day == 0) { throw new Error("Malformed values in BYDAY part"); } } else if (this.has_by_data("BYMONTHDAY")) { if (this.last.day < 0) { var daysInMonth = ICAL.icaltime.daysInMonth(this.last.month, this.last.year); this.last.day = daysInMonth + 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)) { //XXX: right now this is just a flag and has no impact // we can simplify the above case to check for completed later. this.completed = true; return null; } if (this.occurrence_number == 0 && this.last.compare(this.dtstart) >= 0) { // First of all, give the instance that was initialized this.occurrence_number++; return this.last; } do { var valid = 1; switch (this.rule.freq) { case "SECONDLY": this.next_second(); break; case "MINUTELY": this.next_minute(); break; case "HOURLY": this.next_hour(); break; case "DAILY": this.next_day(); break; case "WEEKLY": this.next_week(); break; case "MONTHLY": valid = this.next_month(); break; case "YEARLY": this.next_year(); break; default: return null; } } while (!this.check_contracting_rules() || this.last.compare(this.dtstart) < 0 || !valid); // TODO is this valid? if (this.last.compare(before) == 0) { throw new Error("Same occurrence found twice, protecting " + "you from death by recursion"); } if (this.rule.until && this.last.compare(this.rule.until) > 0) { this.completed = true; return null; } else { this.occurrence_number++; return this.last; } }, next_second: function next_second() { return this.next_generic("BYSECOND", "SECONDLY", "second", "minute"); }, increment_second: function increment_second(inc) { return this.increment_generic(inc, "second", 60, "minute"); }, next_minute: function next_minute() { return this.next_generic("BYMINUTE", "MINUTELY", "minute", "hour", "next_second"); }, increment_minute: function increment_minute(inc) { return this.increment_generic(inc, "minute", 60, "hour"); }, next_hour: function next_hour() { return this.next_generic("BYHOUR", "HOURLY", "hour", "monthday", "next_minute"); }, increment_hour: function increment_hour(inc) { this.increment_generic(inc, "hour", 24, "monthday"); }, next_day: function next_day() { var has_by_day = ("BYDAY" in this.by_data); var this_freq = (this.rule.freq == "DAILY"); if (this.next_hour() == 0) { return 0; } if (this_freq) { this.increment_monthday(this.rule.interval); } else { this.increment_monthday(1); } return 0; }, next_week: function next_week() { var end_of_data = 0; if (this.next_weekday_by_week() == 0) { return end_of_data; } if (this.has_by_data("BYWEEKNO")) { var idx = ++this.by_indices.BYWEEKNO; if (this.by_indices.BYWEEKNO == this.by_data.BYWEEKNO.length) { this.by_indices.BYWEEKNO = 0; end_of_data = 1; } // HACK should be first month of the year this.last.month = 1; this.last.day = 1; var week_no = this.by_data.BYWEEKNO[this.by_indices.BYWEEKNO]; this.last.day += 7 * week_no; 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; }, /** * normalize each by day rule for a given year/month. * Takes into account ordering and negative rules * * @param {Numeric} year current year. * @param {Numeric} month current month. * @param {Array} rules array of rules. * * @return {Array} sorted and normalized rules. * Negative rules will be expanded to their * correct positive values for easier processing. */ normalizeByMonthDayRules: function(year, month, rules) { var daysInMonth = ICAL.icaltime.daysInMonth(month, year); // XXX: This is probably bad for performance to allocate // a new array for each month we scan, if possible // we should try to optimize this... var newRules = []; var ruleIdx = 0; var len = rules.length; var rule; for (; ruleIdx < len; ruleIdx++) { rule = rules[ruleIdx]; // if this rule falls outside of given // month discard it. if (Math.abs(rule) > daysInMonth) { continue; } // negative case if (rule < 0) { // we add (not subtract its a negative number) // one from the rule because 1 === last day of month rule = daysInMonth + (rule + 1); } else if (rule === 0) { // skip zero its invalid. continue; } // only add unique items... if (newRules.indexOf(rule) === -1) { newRules.push(rule); } } // unique and sort return newRules.sort(); }, /** * NOTES: * We are given a list of dates in the month (BYMONTHDAY) (23, etc..) * Also we are given a list of days (BYDAY) (MO, 2SU, etc..) when * both conditions match a given date (this.last.day) iteration stops. * * @param {Boolean} [isInit] when given true will not * increment the current day (this.last). */ _byDayAndMonthDay: function(isInit) { var byMonthDay; // setup in initMonth var byDay = this.by_data.BYDAY; var date; var dateIdx = 0; var dateLen; // setup in initMonth var dayIdx = 0; var dayLen = byDay.length; // we are not valid by default var dataIsValid = 0; var daysInMonth; var self = this; function initMonth() { daysInMonth = ICAL.icaltime.daysInMonth( self.last.month, self.last.year ); byMonthDay = self.normalizeByMonthDayRules( self.last.year, self.last.month, self.by_data.BYMONTHDAY ); dateLen = byMonthDay.length; } function nextMonth() { self.last.day = 1; self.increment_month(); initMonth(); dateIdx = 0; dayIdx = 0; } initMonth(); // should come after initMonth if (isInit) { this.last.day -= 1; } while (!dataIsValid) { // find next date var next = byMonthDay[dateIdx++]; // increment the current date. This is really // important otherwise we may fall into the infinite // loop trap. The initial date takes care of the case // where the current date is the date we are looking // for. date = this.last.day + 1; if (date > daysInMonth) { nextMonth(); continue; } // after verify that the next date // is in the current month we can increment // it permanently. this.last.day = date; // this logic is dependant on the BYMONTHDAYS // being in order (which is done by #normalizeByMonthDayRules) if (next >= this.last.day) { // if the next month day is in the future jump to it. this.last.day = next; } else { // in this case the 'next' monthday has past // we must move to the month. nextMonth(); continue; } // Now we can loop through the day rules to see // if one matches the current month date. for (dayIdx = 0; dayIdx < dayLen; dayIdx++) { var parts = this.ruleDayOfWeek(byDay[dayIdx]); var pos = parts[0]; var dow = parts[1]; if (this.last.isNthWeekDay(dow, pos)) { // when we find the valid one we can mark // the conditions as met and break the loop. // (Because we have this condition above // it will also break the parent loop). dataIsValid = 1; break; } } // Its completely possible that the combination // cannot be matched in the current month. // When we reach the end of possible combinations // in the current month we iterate to the next one. if (!dataIsValid && dateIdx === (dateLen - 1)) { nextMonth(); continue; } } return dataIsValid; }, next_month: function next_month() { var this_freq = (this.rule.freq == "MONTHLY"); var data_valid = 1; if (this.next_hour() == 0) { return data_valid; } if (this.has_by_data("BYDAY") && this.has_by_data("BYMONTHDAY")) { data_valid = this._byDayAndMonthDay(); } else if (this.has_by_data("BYDAY")) { var daysInMonth = ICAL.icaltime.daysInMonth(this.last.month, this.last.year); var setpos = 0; if (this.has_by_data("BYSETPOS")) { var last_day = this.last.day; for (var day = 1; day <= daysInMonth; day++) { this.last.day = day; if (this.is_day_in_byday(this.last) && day <= last_day) { setpos++; } } this.last.day = last_day; } for (var day = this.last.day + 1; day <= daysInMonth; day++) { this.last.day = day; if (this.is_day_in_byday(this.last)) { if (!this.has_by_data("BYSETPOS") || this.check_set_position(++setpos) || this.check_set_position(setpos - this.by_data.BYSETPOS.length - 1)) { data_valid = 1; break; } } } if (day > daysInMonth) { this.last.day = 1; this.increment_month(); if (this.is_day_in_byday(this.last)) { if (!this.has_by_data("BYSETPOS") || this.check_set_position(1)) { data_valid = 1; } } else { data_valid = 0; } } } else if (this.has_by_data("BYMONTHDAY")) { this.by_indices.BYMONTHDAY++; if (this.by_indices.BYMONTHDAY >= this.by_data.BYMONTHDAY.length) { this.by_indices.BYMONTHDAY = 0; this.increment_month(); } var daysInMonth = ICAL.icaltime.daysInMonth(this.last.month, this.last.year); var day = this.by_data.BYMONTHDAY[this.by_indices.BYMONTHDAY]; if (day < 0) { day = daysInMonth + day + 1; } if (day > daysInMonth) { this.last.day = 1; data_valid = this.is_day_in_byday(this.last); } this.last.day = day; } else { this.last.day = this.by_data.BYMONTHDAY[0]; this.increment_month(); var daysInMonth = ICAL.icaltime.daysInMonth(this.last.month, this.last.year); this.last.day = Math.min(this.last.day, daysInMonth); } return data_valid; }, next_weekday_by_week: function next_weekday_by_week() { var end_of_data = 0; if (this.next_hour() == 0) { return end_of_data; } if (!this.has_by_data("BYDAY")) { return 1; } for (;;) { var tt = new ICAL.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.ruleDayOfWeek(coded_day); var dow = parts[1]; dow -= this.rule.wkst; if (dow < 0) { dow += 7; } tt.year = this.last.year; tt.month = this.last.month; tt.day = this.last.day; var startOfWeek = tt.start_doy_week(this.rule.wkst); if (dow + startOfWeek < 1) { // The selected date is in the previous year if (!end_of_data) { continue; } } var next = ICAL.icaltime.fromDayOfYear(startOfWeek + 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.fromDayOfYear(this.days[this.days_index], this.last.year); this.last.day = next.day; this.last.month = next.month; return 1; }, ruleDayOfWeek: function ruleDayOfWeek(dow) { var matches = dow.match(/([+-]?[0-9])?(MO|TU|WE|TH|FR|SA|SU)/); if (matches) { var pos = parseInt(matches[1] || 0, 10); dow = ICAL.icalrecur.icalDayToNumericDay(matches[2]); return [pos, dow]; } else { return [0, 0]; } }, next_generic: function next_generic(aRuleType, aInterval, aDateAttr, aFollowingAttr, aPreviousIncr) { var has_by_rule = (aRuleType in this.by_data); var this_freq = (this.rule.freq == aInterval); var end_of_data = 0; if (aPreviousIncr && this[aPreviousIncr]() == 0) { return end_of_data; } if (has_by_rule) { this.by_indices[aRuleType]++; var idx = this.by_indices[aRuleType]; var dta = this.by_data[aRuleType]; if (this.by_indices[aRuleType] == dta.length) { this.by_indices[aRuleType] = 0; end_of_data = 1; } this.last[aDateAttr] = dta[this.by_indices[aRuleType]]; } else if (this_freq) { this["increment_" + aDateAttr](this.rule.interval); } if (has_by_rule && end_of_data && this_freq) { this["increment_" + aFollowingAttr](1); } return end_of_data; }, increment_monthday: function increment_monthday(inc) { for (var i = 0; i < inc; i++) { var daysInMonth = ICAL.icaltime.daysInMonth(this.last.month, this.last.year); this.last.day++; if (this.last.day > daysInMonth) { this.last.day -= daysInMonth; this.increment_month(); } } }, increment_month: function increment_month() { 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.daysInMonth(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.dayOfYear()); } else if (partCount == 1 && "BYMONTH" in parts) { for (var monthkey in this.by_data.BYMONTH) { var t2 = this.dtstart.clone(); t2.year = aYear; t2.month = this.by_data.BYMONTH[monthkey]; t2.isDate = true; this.days.push(t2.dayOfYear()); } } else if (partCount == 1 && "BYMONTHDAY" in parts) { for (var monthdaykey in this.by_data.BYMONTHDAY) { var t2 = this.dtstart.clone(); t2.day = this.by_data.BYMONTHDAY[monthdaykey]; t2.year = aYear; t2.isDate = true; this.days.push(t2.dayOfYear()); } } else if (partCount == 2 && "BYMONTHDAY" in parts && "BYMONTH" in parts) { for (var monthkey in this.by_data.BYMONTH) { for (var monthdaykey in this.by_data.BYMONTHDAY) { t.day = this.by_data.BYMONTHDAY[monthdaykey]; t.month = this.by_data.BYMONTH[monthkey]; t.year = aYear; t.isDate = true; this.days.push(t.dayOfYear()); } } } else if (partCount == 1 && "BYWEEKNO" in parts) { // TODO unimplemented in libical } else if (partCount == 2 && "BYWEEKNO" in parts && "BYMONTHDAY" in parts) { // TODO unimplemented in libical } else if (partCount == 1 && "BYDAY" in parts) { this.days = this.days.concat(this.expand_by_day(aYear)); } else if (partCount == 2 && "BYDAY" in parts && "BYMONTH" in parts) { for (var monthkey in this.by_data.BYMONTH) { month = this.by_data.BYMONTH[monthkey]; var daysInMonth = ICAL.icaltime.daysInMonth(month, aYear); t.year = aYear; t.month = this.by_data.BYMONTH[monthkey]; t.day = 1; t.isDate = true; var first_dow = t.dayOfWeek(); var doy_offset = t.dayOfYear() - 1; t.day = daysInMonth; var last_dow = t.dayOfWeek(); if (this.has_by_data("BYSETPOS")) { var set_pos_counter = 0; var by_month_day = []; for (var day = 1; day <= daysInMonth; day++) { t.day = day; if (this.is_day_in_byday(t)) { by_month_day.push(day); } } for (var spIndex = 0; spIndex < by_month_day.length; spIndex++) { if (this.check_set_position(spIndex + 1) || this.check_set_position(spIndex - by_month_day.length)) { this.days.push(doy_offset + by_month_day[spIndex]); } } } else { for (var daycodedkey in this.by_data.BYDAY) { //TODO: This should return dates in order of occurrence // (1,2,3, etc...) instead of by weekday (su, mo, etc..) var coded_day = this.by_data.BYDAY[daycodedkey]; var parts = this.ruleDayOfWeek(coded_day); var pos = parts[0]; var dow = parts[1]; var month_day; var first_matching_day = ((dow + 7 - first_dow) % 7) + 1; var last_matching_day = daysInMonth - ((last_dow + 7 - dow) % 7); if (pos == 0) { for (var day = first_matching_day; day <= daysInMonth; day += 7) { this.days.push(doy_offset + day); } } else if (pos > 0) { month_day = first_matching_day + (pos - 1) * 7; if (month_day <= daysInMonth) { this.days.push(doy_offset + month_day); } } else { month_day = last_matching_day + (pos + 1) * 7; if (month_day > 0) { this.days.push(doy_offset + month_day); } } } } } } else if (partCount == 2 && "BYDAY" in parts && "BYMONTHDAY" in parts) { var expandedDays = this.expand_by_day(aYear); for (var daykey in expandedDays) { var day = expandedDays[daykey]; var tt = ICAL.icaltime.fromDayOfYear(day, aYear); if (this.by_data.BYMONTHDAY.indexOf(tt.day) >= 0) { this.days.push(day); } } } else if (partCount == 3 && "BYDAY" in parts && "BYMONTHDAY" in parts && "BYMONTH" in parts) { var expandedDays = this.expand_by_day(aYear); for (var daykey in expandedDays) { var day = expandedDays[daykey]; var tt = ICAL.icaltime.fromDayOfYear(day, aYear); if (this.by_data.BYMONTH.indexOf(tt.month) >= 0 && this.by_data.BYMONTHDAY.indexOf(tt.day) >= 0) { this.days.push(day); } } } else if (partCount == 2 && "BYDAY" in parts && "BYWEEKNO" in parts) { var expandedDays = this.expand_by_day(aYear); for (var daykey in expandedDays) { var day = expandedDays[daykey]; var tt = ICAL.icaltime.fromDayOfYear(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.dayOfWeek(); tmp.month = 12; tmp.day = 31; tmp.isDate = true; var end_dow = tmp.dayOfWeek(); var end_year_day = tmp.dayOfYear(); for (var daykey in this.by_data.BYDAY) { var day = this.by_data.BYDAY[daykey]; var parts = this.ruleDayOfWeek(day); var pos = parts[0]; var dow = parts[1]; if (pos == 0) { var tmp_start_doy = ((dow + 7 - start_dow) % 7) + 1; for (var doy = tmp_start_doy; doy <= end_year_day; doy += 7) { days_list.push(doy); } } else if (pos > 0) { var first; if (dow >= start_dow) { first = dow - start_dow + 1; } else { first = dow - start_dow + 8; } days_list.push(first + (pos - 1) * 7); } else { var last; pos = -pos; if (dow <= end_dow) { last = end_year_day - end_dow + dow; } else { last = end_year_day - end_dow + dow - 7; } days_list.push(last - (pos - 1) * 7); } } return days_list; }, is_day_in_byday: function is_day_in_byday(tt) { for (var daykey in this.by_data.BYDAY) { var day = this.by_data.BYDAY[daykey]; var parts = this.ruleDayOfWeek(day); var pos = parts[0]; var dow = parts[1]; var this_dow = tt.dayOfWeek(); if ((pos == 0 && dow == this_dow) || (tt.nthWeekDay(dow, pos) == tt.day)) { return 1; } } return 0; }, /** * Checks if given value is in BYSETPOS. * * @param {Numeric} aPos position to check for. * @return {Boolean} false unless BYSETPOS rules exist * and the given value is present in rules. */ check_set_position: function check_set_position(aPos) { if (this.has_by_data('BYSETPOS')) { var idx = this.by_data.BYSETPOS.indexOf(aPos); // negative numbers are not false-y return idx !== -1; } return false; }, sort_byday_rules: function icalrecur_sort_byday_rules(aRules, aWeekStart) { for (var i = 0; i < aRules.length; i++) { for (var j = 0; j < i; j++) { var one = this.ruleDayOfWeek(aRules[j])[1]; var two = this.ruleDayOfWeek(aRules[i])[1]; one -= aWeekStart; two -= aWeekStart; if (one < 0) one += 7; if (two < 0) two += 7; if (one > two) { var tmp = aRules[i]; aRules[i] = aRules[j]; aRules[j] = tmp; } } } }, check_contract_restriction: function check_contract_restriction(aRuleType, v) { var indexMapValue = icalrecur_iterator._indexMap[aRuleType]; var ruleMapValue = icalrecur_iterator._expandMap[this.rule.freq][indexMapValue]; var pass = false; if (aRuleType in this.by_data && ruleMapValue == icalrecur_iterator.CONTRACT) { var ruleType = this.by_data[aRuleType]; for (var bydatakey in ruleType) { if (ruleType[bydatakey] == v) { pass = true; break; } } } else { // Not a contracting byrule or has no data, test passes pass = true; } return pass; }, check_contracting_rules: function check_contracting_rules() { var dow = this.last.dayOfWeek(); var weekNo = this.last.week_number(this.rule.wkst); var doy = this.last.dayOfYear(); return (this.check_contract_restriction("BYSECOND", this.last.second) && this.check_contract_restriction("BYMINUTE", this.last.minute) && this.check_contract_restriction("BYHOUR", this.last.hour) && this.check_contract_restriction("BYDAY", icalrecur_iterator._wkdayMap[dow]) && this.check_contract_restriction("BYWEEKNO", weekNo) && this.check_contract_restriction("BYMONTHDAY", this.last.day) && this.check_contract_restriction("BYMONTH", this.last.month) && this.check_contract_restriction("BYYEARDAY", doy)); }, setup_defaults: function setup_defaults(aRuleType, req, deftime) { var indexMapValue = icalrecur_iterator._indexMap[aRuleType]; var ruleMapValue = icalrecur_iterator._expandMap[this.rule.freq][indexMapValue]; if (ruleMapValue != icalrecur_iterator.CONTRACT) { if (!(aRuleType in this.by_data)) { this.by_data[aRuleType] = [deftime]; } if (this.rule.freq != req) { return this.by_data[aRuleType][0]; } } return deftime; }, /** * Convert iterator into a serialize-able object. * Will preserve current iteration sequence to ensure * the seamless continuation of the recurrence rule. */ toJSON: function() { var result = Object.create(null); result.initialized = this.initialized; result.rule = this.rule.toJSON(); result.dtstart = this.dtstart.toJSON(); result.by_data = this.by_data; result.days = this.days; result.last = this.last.toJSON(); result.by_indices = this.by_indices; result.occurrence_number = this.occurrence_number; return result; } }; icalrecur_iterator._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; return icalrecur_iterator; }()); ICAL.RecurExpansion = (function() { function formatTime(item) { return ICAL.helpers.formatClassType(item, ICAL.icaltime); } function compareTime(a, b) { return a.compare(b); } function isRecurringComponent(comp) { return comp.hasProperty('rdate') || comp.hasProperty('rrule') || comp.hasProperty('recurrence-id'); } /** * Primary class for expanding recurring rules. * Can take multiple rrules, rdates, exdate(s) * and iterate (in order) over each next occurrence. * * Once initialized this class can also be serialized * saved and continue iteration from the last point. * * NOTE: it is intended that this class is to be used * with ICAL.Event which handles recurrence exceptions. * * Options: * - dtstart: (ICAL.icaltime) start time of event (required) * - component: (ICAL.Component) component (required unless resuming) * * Examples: * * // assuming event is a parsed ical component * var event; * * var expand = new ICAL.RecurExpansion({ * component: event, * start: event.getFirstPropertyValue('DTSTART') * }); * * // remember there are infinite rules * // so its a good idea to limit the scope * // of the iterations then resume later on. * * // next is always an ICAL.icaltime or null * var next; * * while(someCondition && (next = expand.next())) { * // do something with next * } * * // save instance for later * var json = JSON.stringify(expand); * * //... * * // NOTE: if the component's properties have * // changed you will need to rebuild the * // class and start over. This only works * // when the component's recurrence info is the same. * var expand = new ICAL.RecurExpansion(JSON.parse(json)); * * * @param {Object} options see options block. */ function RecurExpansion(options) { this.ruleDates = []; this.exDates = []; this.fromData(options); } RecurExpansion.prototype = { /** * True when iteration is fully completed. */ complete: false, /** * Array of rrule iterators. * * @type Array[ICAL.icalrecur_iterator] * @private */ ruleIterators: null, /** * Array of rdate instances. * * @type Array[ICAL.icaltime] * @private */ ruleDates: null, /** * Array of exdate instances. * * @type Array[ICAL.icaltime] * @private */ exDates: null, /** * Current position in ruleDates array. * @type Numeric * @private */ ruleDateInc: 0, /** * Current position in exDates array * @type Numeric * @private */ exDateInc: 0, /** * Current negative date. * * @type ICAL.icaltime * @private */ exDate: null, /** * Current additional date. * * @type ICAL.icaltime * @private */ ruleDate: null, /** * Start date of recurring rules. * * @type ICAL.icaltime */ dtstart: null, /** * Last expanded time * * @type ICAL.icaltime */ last: null, fromData: function(options) { var start = ICAL.helpers.formatClassType(options.dtstart, ICAL.icaltime); if (!start) { throw new Error('.dtstart (ICAL.icaltime) must be given'); } else { this.dtstart = start; } if (options.component) { this._init(options.component); } else { this.last = formatTime(options.last); this.ruleIterators = options.ruleIterators.map(function(item) { return ICAL.helpers.formatClassType(item, ICAL.icalrecur_iterator); }); this.ruleDateInc = options.ruleDateInc; this.exDateInc = options.exDateInc; if (options.ruleDates) { this.ruleDates = options.ruleDates.map(formatTime); this.ruleDate = this.ruleDates[this.ruleDateInc]; } if (options.exDates) { this.exDates = options.exDates.map(formatTime); this.exDate = this.exDates[this.exDateInc]; } if (typeof(options.complete) !== 'undefined') { this.complete = options.complete; } } }, next: function() { var iter; var ruleOfDay; var next; var compare; var maxTries = 500; var currentTry = 0; while (true) { if (currentTry++ > maxTries) { throw new Error( 'max tries have occured, rule may be impossible to forfill.' ); } next = this.ruleDate; iter = this._nextRecurrenceIter(this.last); // no more matches // because we increment the rule day or rule // _after_ we choose a value this should be // the only spot where we need to worry about the // end of events. if (!next && !iter) { // there are no more iterators or rdates this.complete = true; break; } // no next rule day or recurrence rule is first. if (!next || (iter && next.compare(iter.last) > 0)) { // must be cloned, recur will reuse the time element. next = iter.last.clone(); // move to next so we can continue iter.next(); } // if the ruleDate is still next increment it. if (this.ruleDate === next) { this._nextRuleDay(); } this.last = next; // check the negative rules if (this.exDate) { compare = this.exDate.compare(this.last); if (compare < 0) { this._nextExDay(); } // if the current rule is excluded skip it. if (compare === 0) { this._nextExDay(); continue; } } //XXX: The spec states that after we resolve the final // list of dates we execute exdate this seems somewhat counter // intuitive to what I have seen most servers do so for now // I exclude based on the original date not the one that may // have been modified by the exception. return this.last; } }, /** * Converts object into a serialize-able format. */ toJSON: function() { function toJSON(item) { return item.toJSON(); } var result = Object.create(null); result.ruleIterators = this.ruleIterators.map(toJSON); if (this.ruleDates) { result.ruleDates = this.ruleDates.map(toJSON); } if (this.exDates) { result.exDates = this.exDates.map(toJSON); } result.ruleDateInc = this.ruleDateInc; result.exDateInc = this.exDateInc; result.last = this.last.toJSON(); result.dtstart = this.dtstart.toJSON(); result.complete = this.complete; return result; }, _extractDates: function(component, property) { var result = []; var props = component.getAllProperties(property); var len = props.length; var i = 0; var prop; var idx; for (; i < len; i++) { prop = props[i].getFirstValue(); idx = ICAL.helpers.binsearchInsert( result, prop, compareTime ); // ordered insert result.splice(idx, 0, prop); } return result; }, _init: function(component) { this.ruleIterators = []; this.last = this.dtstart.clone(); // to provide api consistency non-recurring // events can also use the iterator though it will // only return a single time. if (!isRecurringComponent(component)) { this.ruleDate = this.last.clone(); this.complete = true; return; } if (component.hasProperty('rrule')) { var rules = component.getAllProperties('rrule'); var i = 0; var len = rules.length; var rule; var iter; for (; i < len; i++) { rule = rules[i].getFirstValue(); iter = rule.iterator(this.dtstart); this.ruleIterators.push(iter); // increment to the next occurrence so future // calls to next return times beyond the initial iteration. // XXX: I find this suspicious might be a bug? iter.next(); } } if (component.hasProperty('rdate')) { this.ruleDates = this._extractDates(component, 'rdate'); this.ruleDateInc = ICAL.helpers.binsearchInsert( this.ruleDates, this.last, compareTime ); this.ruleDate = this.ruleDates[this.ruleDateInc]; } if (component.hasProperty('exdate')) { this.exDates = this._extractDates(component, 'exdate'); // if we have a .last day we increment the index to beyond it. this.exDateInc = ICAL.helpers.binsearchInsert( this.exDates, this.last, compareTime ); this.exDate = this.exDates[this.exDateInc]; } }, _nextExDay: function() { this.exDate = this.exDates[++this.exDateInc]; }, _nextRuleDay: function() { this.ruleDate = this.ruleDates[++this.ruleDateInc]; }, /** * Find and return the recurrence rule with the most * recent event and return it. * * @return {Object} iterator. */ _nextRecurrenceIter: function() { var iters = this.ruleIterators; if (iters.length === 0) { return null; } var len = iters.length; var iter; var iterTime; var iterIdx = 0; var chosenIter; // loop through each iterator for (; iterIdx < len; iterIdx++) { iter = iters[iterIdx]; iterTime = iter.last; // if iteration is complete // then we must exclude it from // the search and remove it. if (iter.completed) { len--; if (iterIdx !== 0) { iterIdx--; } iters.splice(iterIdx, 1); continue; } // find the most recent possible choice if (!chosenIter || chosenIter.last.compare(iterTime) > 0) { // that iterator is saved chosenIter = iter; } } // the chosen iterator is returned but not mutated // this iterator contains the most recent event. return chosenIter; } }; return RecurExpansion; }()); ICAL.Event = (function() { function Event(component, options) { if (!(component instanceof ICAL.Component)) { options = component; component = null; } if (component) { this.component = component; } else { this.component = new ICAL.Component('vevent'); } this.exceptions = Object.create(null); if (options && options.exceptions) { options.exceptions.forEach(this.relateException, this); } } Event.prototype = { /** * List of related event exceptions. * * @type Array[ICAL.Event] */ exceptions: null, /** * Relates a given event exception to this object. * If the given component does not share the UID of * this event it cannot be related and will throw an * exception. * * If this component is an exception it cannot have other * exceptions related to it. * * @param {ICAL.Component|ICAL.Event} obj component or event. */ relateException: function(obj) { if (this.isRecurrenceException()) { throw new Error('cannot relate exception to exceptions'); } if (obj instanceof ICAL.Component) { obj = new ICAL.Event(obj); } if (obj.uid !== this.uid) { throw new Error('attempted to relate unrelated exception'); } // we don't sort or manage exceptions directly // here the recurrence expander handles that. this.exceptions[obj.recurrenceId.toString()] = obj; }, /** * Returns the occurrence details based on its start time. * If the occurrence has an exception will return the details * for that exception. * * NOTE: this method is intend to be used in conjunction * with the #iterator method. * * @param {ICAL.icaltime} occurrence time occurrence. */ getOccurrenceDetails: function(occurrence) { var id = occurrence.toString(); var result = { //XXX: Clone? recurrenceId: occurrence }; if (id in this.exceptions) { var item = result.item = this.exceptions[id]; result.startDate = item.startDate; result.endDate = item.endDate; result.item = item; } else { var end = occurrence.clone(); end.addDuration(this.duration); result.endDate = end; result.startDate = occurrence; result.item = this; } return result; }, /** * Builds a recur expansion instance for a specific * point in time (defaults to startDate). * * @return {ICAL.RecurExpansion} expander object. */ iterator: function(startTime) { return new ICAL.RecurExpansion({ component: this.component, dtstart: startTime || this.startDate }); }, isRecurring: function() { var comp = this.component; return comp.hasProperty('rrule') || comp.hasProperty('rdate'); }, isRecurrenceException: function() { return this.component.hasProperty('recurrence-id'); }, /** * Returns the types of recurrences this event may have. * * Returned as an object with the following possible keys: * * - YEARLY * - MONTHLY * - WEEKLY * - DAILY * - MINUTELY * - SECONDLY * * @return {Object} object of recurrence flags. */ getRecurrenceTypes: function() { var rules = this.component.getAllProperties('rrule'); var i = 0; var len = rules.length; var result = Object.create(null); for (; i < len; i++) { var value = rules[i].getFirstValue(); result[value.freq] = true; } return result; }, get uid() { return this._firstProp('uid'); }, set uid(value) { this._setProp('uid', value); }, get startDate() { return this._firstProp('dtstart'); }, set startDate(value) { this._setProp('dtstart', value); }, get endDate() { return this._firstProp('dtend'); }, set endDate(value) { this._setProp('dtend', value); }, get duration() { // cached because its dynamically calculated // and may be frequently used. This could be problematic // later if we modify the underlying start/endDate. // // When do add that functionality it should expire this cache... if (typeof(this._duration) === 'undefined') { this._duration = this.endDate.subtractDate(this.startDate); } return this._duration; }, get location() { return this._firstProp('location'); }, set location(value) { return this._setProp('location', value); }, get attendees() { //XXX: This is way lame we should have a better // data structure for this later. return this.component.getAllProperties('attendee'); }, get summary() { return this._firstProp('summary'); }, set summary(value) { this._setProp('summary', value); }, get description() { return this._firstProp('description'); }, set description(value) { this._setProp('description', value); }, get organizer() { return this._firstProp('organizer'); }, set organizer(value) { this._setProp('organizer', value); }, get sequence() { return this._firstProp('sequence'); }, set sequence(value) { this._setProp('sequence', value); }, get recurrenceId() { return this._firstProp('recurrence-id'); }, set recurrenceId(value) { this._setProp('recurrence-id', value); }, _setProp: function(name, value) { this.component.updatePropertyWithValue(name, value); }, _firstProp: function(name) { return this.component.getFirstPropertyValue(name); }, toString: function() { return this.component.toString(); } }; return Event; }()); ICAL.ComponentParser = (function() { /** * Component parser initializer. * * Usage: * * var options = { * // when false no events will be emitted for type * parseEvent: true, * parseTimezone: true * }; * * var parser = new ICAL.ComponentParser(options); * * parser.onevent() { * //... * } * * // ontimezone, etc... * * parser.oncomplete = function() { * * }; * * parser.process(string | component); * * * @param {Object} options component parser options. */ function ComponentParser(options) { if (typeof(options) === 'undefined') { options = {}; } var key; for (key in options) { if (options.hasOwnProperty(key)) { this[key] = options[key]; } } } ComponentParser.prototype = { /** * When true parse events * * @type Boolean */ parseEvent: true, /** * when true parse timezones * * @type Boolean */ parseTimezone: true, /* SAX like events here for reference */ /** * Fired when parsing is complete */ oncomplete: function() {}, /** * Fired if an error occurs during parsing. * * @param {Error} err details of error. */ onerror: function(err) {}, /** * Fired when a top level component (vtimezone) is found * * @param {ICAL.icaltimezone} timezone object. */ ontimezone: function(component) {}, /* * Fired when a top level component (VEVENT) is found. * @param {ICAL.Event} component top level component. */ onevent: function(component) {}, /** * Process a string or parse ical object. * This function itself will return nothing but * will start the parsing process. * * Events must be registered prior to calling this method. * * @param {String|Object} ical string or parsed ical object. */ process: function(ical) { //TODO: this is sync now in the future we will have a incremental parser. if (typeof(ical) === 'string') { ical = ICAL.parse(ical)[1]; } if (!(ical instanceof ICAL.Component)) { ical = new ICAL.Component(ical); } var components = ical.getAllSubcomponents(); var i = 0; var len = components.length; var component; for (; i < len; i++) { component = components[i]; switch (component.name) { case 'vevent': if (this.parseEvent) { this.onevent(new ICAL.Event(component)); } break; default: continue; } } //XXX: ideally we should do a "nextTick" here // so in all cases this is actually async. this.oncomplete(); } }; return ComponentParser; }());