/* 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-2015 */ /* istanbul ignore next */ /* jshint ignore:start */ if (typeof module === 'object') { // CommonJS, where exports may be different each time. ICAL = module.exports; } else if (typeof ICAL !== 'object') {/* istanbul ignore next */ /** @ignore */ this.ICAL = {}; } /* jshint ignore:end */ /** * The number of characters before iCalendar line folding should occur * @type {Number} * @default 75 */ ICAL.foldLength = 75; /** * The character(s) to be used for a newline. The default value is provided by * rfc5545. * @type {String} * @default "\r\n" */ ICAL.newLineChar = '\r\n'; /** * Helper functions used in various places within ical.js * @namespace */ ICAL.helpers = { /** * Checks if the given type is of the number type and also NaN. * * @param {Number} number The number to check * @return {Boolean} True, if the number is strictly NaN */ isStrictlyNaN: function(number) { return typeof(number) === 'number' && isNaN(number); }, /** * Parses a string value that is expected to be an integer, when the valid is * not an integer throws a decoration error. * * @param {String} string Raw string input * @return {Number} Parsed integer */ strictParseInt: function(string) { var result = parseInt(string, 10); if (ICAL.helpers.isStrictlyNaN(result)) { throw new Error( 'Could not extract integer from "' + string + '"' ); } return result; }, /** * Creates or returns a class instance of a given type with the initialization * data if the data is not already an instance of the given type. * * @example * var time = new ICAL.Time(...); * var result = ICAL.helpers.formatClassType(time, ICAL.Time); * * (result instanceof ICAL.Time) * // => true * * result = ICAL.helpers.formatClassType({}, ICAL.Time); * (result isntanceof ICAL.Time) * // => true * * * @param {Object} data object initialization data * @param {Object} type object type (like ICAL.Time) * @return {?} An instance of the found type. */ formatClassType: function formatClassType(data, type) { if (typeof(data) === 'undefined') { return undefined; } if (data instanceof type) { return data; } return new type(data); }, /** * Identical to indexOf but will only match values when they are not preceded * by a backslash character. * * @param {String} buffer String to search * @param {String} search Value to look for * @param {Number} pos Start position * @return {Number} The position, or -1 if not found */ 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; }, /** * Find the index for insertion using binary search. * * @param {Array} list The list to search * @param {?} seekVal The value to insert * @param {function(?,?)} cmpfunc The comparison func, that can * compare two seekVals * @return {Number} The insert position */ 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; }, /** * Convenience function for debug output * @private */ dumpn: /* istanbul ignore next */ function() { if (!ICAL.debug) { return; } if (typeof (console) !== 'undefined' && 'log' in console) { ICAL.helpers.dumpn = function consoleDumpn(input) { console.log(input); }; } else { ICAL.helpers.dumpn = function geckoDumpn(input) { dump(input + '\n'); }; } ICAL.helpers.dumpn(arguments[0]); }, /** * Clone the passed object or primitive. By default a shallow clone will be * executed. * * @param {*} aSrc The thing to clone * @param {Boolean=} aDeep If true, a deep clone will be performed * @return {*} The copy of the thing */ 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 (Array.isArray(aSrc)) { var arr = []; for (var i = 0; i < aSrc.length; i++) { arr.push(aDeep ? ICAL.helpers.clone(aSrc[i], true) : aSrc[i]); } return arr; } else { var obj = {}; for (var name in aSrc) { // uses prototype method to allow use of Object.create(null); /* istanbul ignore else */ if (Object.prototype.hasOwnProperty.call(aSrc, name)) { if (aDeep) { obj[name] = ICAL.helpers.clone(aSrc[name], true); } else { obj[name] = aSrc[name]; } } } return obj; } }, /** * Performs iCalendar line folding. A line ending character is inserted and * the next line begins with a whitespace. * * @example * SUMMARY:This line will be fold * ed right in the middle of a word. * * @param {String} aLine The line to fold * @return {String} The folded 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); }, /** * Pads the given string or number with zeros so it will have at least two * characters. * * @param {String|Number} data The string or number to pad * @return {String} The number padded as a string */ pad2: function pad(data) { if (typeof(data) !== 'string') { // handle fractions. if (typeof(data) === 'number') { data = parseInt(data); } data = String(data); } var len = data.length; switch (len) { case 0: return '00'; case 1: return '0' + data; default: return data; } }, /** * Truncates the given number, correctly handling negative numbers. * * @param {Number} number The number to truncate * @return {Number} The truncated number */ trunc: function trunc(number) { return (number < 0 ? Math.ceil(number) : Math.floor(number)); }, /** * Poor-man's cross-browser inheritance for JavaScript. Doesn't support all * the features, but enough for our usage. * * @param {Function} base The base class constructor function. * @param {Function} child The child class constructor function. * @param {Object} extra Extends the prototype with extra properties * and methods */ inherits: function(base, child, extra) { function F() {} F.prototype = base.prototype; child.prototype = new F(); if (extra) { ICAL.helpers.extend(extra, child.prototype); } }, /** * Poor-man's cross-browser object extension. Doesn't support all the * features, but enough for our usage. Note that the target's properties are * not overwritten with the source properties. * * @example * var child = ICAL.helpers.extend(parent, { * "bar": 123 * }); * * @param {Object} source The object to extend * @param {Object} target The object to extend with * @return {Object} Returns the target. */ extend: function(source, target) { for (var key in source) { var descr = Object.getOwnPropertyDescriptor(source, key); if (descr && !Object.getOwnPropertyDescriptor(target, key)) { Object.defineProperty(target, key, descr); } } return target; } }; /* 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-2015 */ /** @namespace ICAL */ /** * This symbol is further described later on * @ignore */ ICAL.design = (function() { 'use strict'; var FROM_ICAL_NEWLINE = /\\\\|\\;|\\,|\\[Nn]/g; var TO_ICAL_NEWLINE = /\\|;|,|\n/g; var FROM_VCARD_NEWLINE = /\\\\|\\,|\\[Nn]/g; var TO_VCARD_NEWLINE = /\\|,|\n/g; function createTextType(fromNewline, toNewline) { var result = { matches: /.*/, fromICAL: function(aValue, structuredEscape) { return replaceNewline(aValue, fromNewline, structuredEscape); }, toICAL: function(aValue, structuredEscape) { var regEx = toNewline; if (structuredEscape) regEx = new RegExp(regEx.source + '|' + structuredEscape); return aValue.replace(regEx, function(str) { switch (str) { case "\\": return "\\\\"; case ";": return "\\;"; case ",": return "\\,"; case "\n": return "\\n"; /* istanbul ignore next */ default: return str; } }); } }; return result; } // default types used multiple times var DEFAULT_TYPE_TEXT = { defaultType: "text" }; var DEFAULT_TYPE_TEXT_MULTI = { defaultType: "text", multiValue: "," }; var DEFAULT_TYPE_TEXT_STRUCTURED = { defaultType: "text", structuredValue: ";" }; var DEFAULT_TYPE_INTEGER = { defaultType: "integer" }; var DEFAULT_TYPE_DATETIME_DATE = { defaultType: "date-time", allowedTypes: ["date-time", "date"] }; var DEFAULT_TYPE_DATETIME = { defaultType: "date-time" }; var DEFAULT_TYPE_URI = { defaultType: "uri" }; var DEFAULT_TYPE_UTCOFFSET = { defaultType: "utc-offset" }; var DEFAULT_TYPE_RECUR = { defaultType: "recur" }; var DEFAULT_TYPE_DATE_ANDOR_TIME = { defaultType: "date-and-or-time", allowedTypes: ["date-time", "date", "text"] }; function replaceNewlineReplace(string) { switch (string) { case "\\\\": return "\\"; case "\\;": return ";"; case "\\,": return ","; case "\\n": case "\\N": return "\n"; /* istanbul ignore next */ default: return string; } } function replaceNewline(value, newline, structuredEscape) { // avoid regex when possible. if (value.indexOf('\\') === -1) { return value; } if (structuredEscape) newline = new RegExp(newline.source + '|\\\\' + structuredEscape); return value.replace(newline, replaceNewlineReplace); } var commonProperties = { "categories": DEFAULT_TYPE_TEXT_MULTI, "url": DEFAULT_TYPE_URI, "version": DEFAULT_TYPE_TEXT, "uid": DEFAULT_TYPE_TEXT }; var commonValues = { "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'; } }, float: { matches: /^[+-]?\d+\.\d+$/, fromICAL: function(aValue) { var parsed = parseFloat(aValue); if (ICAL.helpers.isStrictlyNaN(parsed)) { // TODO: parser warning return 0.0; } return parsed; }, toICAL: function(aValue) { return String(aValue); } }, integer: { fromICAL: function(aValue) { var parsed = parseInt(aValue); if (ICAL.helpers.isStrictlyNaN(parsed)) { return 0; } return parsed; }, toICAL: function(aValue) { return String(aValue); } }, "utc-offset": { toICAL: function(aValue) { if (aValue.length < 7) { // no seconds // -0500 return aValue.substr(0, 3) + aValue.substr(4, 2); } else { // seconds // -050000 return aValue.substr(0, 3) + aValue.substr(4, 2) + aValue.substr(7, 2); } }, fromICAL: function(aValue) { if (aValue.length < 6) { // no seconds // -05:00 return aValue.substr(0, 3) + ':' + aValue.substr(3, 2); } else { // seconds // -05:00:00 return aValue.substr(0, 3) + ':' + aValue.substr(3, 2) + ':' + aValue.substr(5, 2); } }, decorate: function(aValue) { return ICAL.UtcOffset.fromString(aValue); }, undecorate: function(aValue) { return aValue.toString(); } } }; var icalParams = { // Although the syntax is DQUOTE uri DQUOTE, I don't think we should // enfoce anything aside from it being a valid content line. // // At least some params require - if multi values are used - DQUOTEs // for each of its values - e.g. delegated-from="uri1","uri2" // To indicate this, I introduced the new k/v pair // multiValueSeparateDQuote: true // // "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: ",", multiValueSeparateDQuote: true }, "delegated-to": { valueType: "cal-address", multiValue: ",", multiValueSeparateDQuote: true }, // "DIR": { ... }, // See ALTREP "encoding": { values: ["8BIT", "BASE64"] }, // "FMTTYPE": { ... }, // See ALTREP "fbtype": { values: ["FREE", "BUSY", "BUSY-UNAVAILABLE", "BUSY-TENTATIVE"], allowXName: true, allowIanaToken: true }, // "LANGUAGE": { ... }, // See ALTREP "member": { valueType: "cal-address", multiValue: ",", multiValueSeparateDQuote: true }, "partstat": { // TODO These values are actually different per-component values: ["NEEDS-ACTION", "ACCEPTED", "DECLINED", "TENTATIVE", "DELEGATED", "COMPLETED", "IN-PROCESS"], allowXName: true, allowIanaToken: true }, "range": { values: ["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": { values: ["TRUE", "FALSE"] }, "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! var icalValues = ICAL.helpers.extend(commonValues, { text: createTextType(FROM_ICAL_NEWLINE, TO_ICAL_NEWLINE), uri: { // TODO /* ... */ }, "binary": { decorate: function(aString) { return ICAL.Binary.fromString(aString); }, undecorate: function(aBinary) { return aBinary.toString(); } }, "cal-address": { // needs to be an uri }, "date": { decorate: function(aValue, aProp) { return ICAL.Time.fromDateString(aValue, aProp); }, /** * undecorates a time object. */ undecorate: function(aValue) { return aValue.toString(); }, fromICAL: function(aValue) { // from: 20120901 // to: 2012-09-01 var result = aValue.substr(0, 4) + '-' + aValue.substr(4, 2) + '-' + aValue.substr(6, 2); if (aValue[8] === 'Z') { result += 'Z'; } return result; }, toICAL: function(aValue) { // from: 2012-09-01 // to: 20120901 if (aValue.length > 11) { //TODO: serialize warning? return aValue; } var result = aValue.substr(0, 4) + aValue.substr(5, 2) + aValue.substr(8, 2); if (aValue[10] === 'Z') { result += 'Z'; } return result; } }, "date-time": { fromICAL: function(aValue) { // from: 20120901T130000 // to: 2012-09-01T13:00:00 var result = aValue.substr(0, 4) + '-' + aValue.substr(4, 2) + '-' + aValue.substr(6, 2) + 'T' + aValue.substr(9, 2) + ':' + aValue.substr(11, 2) + ':' + aValue.substr(13, 2); if (aValue[15] && 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] && aValue[19] === 'Z') { result += 'Z'; } return result; }, decorate: function(aValue, aProp) { return ICAL.Time.fromDateTimeString(aValue, aProp); }, undecorate: function(aValue) { return aValue.toString(); } }, duration: { decorate: function(aValue) { return ICAL.Duration.fromString(aValue); }, undecorate: function(aValue) { return aValue.toString(); } }, period: { fromICAL: function(string) { var parts = string.split('/'); parts[0] = icalValues['date-time'].fromICAL(parts[0]); if (!ICAL.Duration.isValueString(parts[1])) { parts[1] = icalValues['date-time'].fromICAL(parts[1]); } return parts; }, toICAL: function(parts) { parts[0] = icalValues['date-time'].toICAL(parts[0]); if (!ICAL.Duration.isValueString(parts[1])) { parts[1] = icalValues['date-time'].toICAL(parts[1]); } return parts.join("/"); }, decorate: function(aValue, aProp) { return ICAL.Period.fromJSON(aValue, aProp); }, undecorate: function(aValue) { return aValue.toJSON(); } }, recur: { fromICAL: function(string) { return ICAL.Recur._stringToData(string, true); }, toICAL: function(data) { var str = ""; for (var k in data) { /* istanbul ignore if */ if (!Object.prototype.hasOwnProperty.call(data, k)) { continue; } var val = data[k]; if (k == "until") { if (val.length > 10) { val = icalValues['date-time'].toICAL(val); } else { val = icalValues.date.toICAL(val); } } else if (k == "wkst") { if (typeof val === 'number') { val = ICAL.Recur.numericDayToIcalDay(val); } } else if (Array.isArray(val)) { val = val.join(","); } str += k.toUpperCase() + "=" + val + ";"; } return str.substr(0, str.length - 1); }, decorate: function decorate(aValue) { return ICAL.Recur.fromData(aValue); }, undecorate: function(aRecur) { return aRecur.toJSON(); } }, time: { fromICAL: function(aValue) { // from: MMHHSS(Z)? // to: HH:MM:SS(Z)? if (aValue.length < 6) { // TODO: parser exception? return aValue; } // HH::MM::SSZ? var result = aValue.substr(0, 2) + ':' + aValue.substr(2, 2) + ':' + aValue.substr(4, 2); if (aValue[6] === 'Z') { result += 'Z'; } return result; }, toICAL: function(aValue) { // from: HH:MM:SS(Z)? // to: MMHHSS(Z)? if (aValue.length < 8) { //TODO: error return aValue; } var result = aValue.substr(0, 2) + aValue.substr(3, 2) + aValue.substr(6, 2); if (aValue[8] === 'Z') { result += 'Z'; } return result; } } }); var icalProperties = ICAL.helpers.extend(commonProperties, { "action": DEFAULT_TYPE_TEXT, "attach": { defaultType: "uri" }, "attendee": { defaultType: "cal-address" }, "calscale": DEFAULT_TYPE_TEXT, "class": DEFAULT_TYPE_TEXT, "comment": DEFAULT_TYPE_TEXT, "completed": DEFAULT_TYPE_DATETIME, "contact": DEFAULT_TYPE_TEXT, "created": DEFAULT_TYPE_DATETIME, "description": DEFAULT_TYPE_TEXT, "dtend": DEFAULT_TYPE_DATETIME_DATE, "dtstamp": DEFAULT_TYPE_DATETIME, "dtstart": DEFAULT_TYPE_DATETIME_DATE, "due": DEFAULT_TYPE_DATETIME_DATE, "duration": { defaultType: "duration" }, "exdate": { defaultType: "date-time", allowedTypes: ["date-time", "date"], multiValue: ',' }, "exrule": DEFAULT_TYPE_RECUR, "freebusy": { defaultType: "period", multiValue: "," }, "geo": { defaultType: "float", structuredValue: ";" }, "last-modified": DEFAULT_TYPE_DATETIME, "location": DEFAULT_TYPE_TEXT, "method": DEFAULT_TYPE_TEXT, "organizer": { defaultType: "cal-address" }, "percent-complete": DEFAULT_TYPE_INTEGER, "priority": DEFAULT_TYPE_INTEGER, "prodid": DEFAULT_TYPE_TEXT, "related-to": DEFAULT_TYPE_TEXT, "repeat": DEFAULT_TYPE_INTEGER, "rdate": { defaultType: "date-time", allowedTypes: ["date-time", "date", "period"], multiValue: ',', detectType: function(string) { if (string.indexOf('/') !== -1) { return 'period'; } return (string.indexOf('T') === -1) ? 'date' : 'date-time'; } }, "recurrence-id": DEFAULT_TYPE_DATETIME_DATE, "resources": DEFAULT_TYPE_TEXT_MULTI, "request-status": DEFAULT_TYPE_TEXT_STRUCTURED, "rrule": DEFAULT_TYPE_RECUR, "sequence": DEFAULT_TYPE_INTEGER, "status": DEFAULT_TYPE_TEXT, "summary": DEFAULT_TYPE_TEXT, "transp": DEFAULT_TYPE_TEXT, "trigger": { defaultType: "duration", allowedTypes: ["duration", "date-time"] }, "tzoffsetfrom": DEFAULT_TYPE_UTCOFFSET, "tzoffsetto": DEFAULT_TYPE_UTCOFFSET, "tzurl": DEFAULT_TYPE_URI, "tzid": DEFAULT_TYPE_TEXT, "tzname": DEFAULT_TYPE_TEXT }); // When adding a value here, be sure to add it to the parameter types! var vcardValues = ICAL.helpers.extend(commonValues, { text: createTextType(FROM_VCARD_NEWLINE, TO_VCARD_NEWLINE), uri: createTextType(FROM_VCARD_NEWLINE, TO_VCARD_NEWLINE), date: { decorate: function(aValue) { return ICAL.VCardTime.fromDateAndOrTimeString(aValue, "date"); }, undecorate: function(aValue) { return aValue.toString(); }, fromICAL: function(aValue) { if (aValue.length == 8) { return icalValues.date.fromICAL(aValue); } else if (aValue[0] == '-' && aValue.length == 6) { return aValue.substr(0, 4) + '-' + aValue.substr(4); } else { return aValue; } }, toICAL: function(aValue) { if (aValue.length == 10) { return icalValues.date.toICAL(aValue); } else if (aValue[0] == '-' && aValue.length == 7) { return aValue.substr(0, 4) + aValue.substr(5); } else { return aValue; } } }, time: { decorate: function(aValue) { return ICAL.VCardTime.fromDateAndOrTimeString("T" + aValue, "time"); }, undecorate: function(aValue) { return aValue.toString(); }, fromICAL: function(aValue) { var splitzone = vcardValues.time._splitZone(aValue, true); var zone = splitzone[0], value = splitzone[1]; //console.log("SPLIT: ",splitzone); if (value.length == 6) { value = value.substr(0, 2) + ':' + value.substr(2, 2) + ':' + value.substr(4, 2); } else if (value.length == 4 && value[0] != '-') { value = value.substr(0, 2) + ':' + value.substr(2, 2); } else if (value.length == 5) { value = value.substr(0, 3) + ':' + value.substr(3, 2); } if (zone.length == 5 && (zone[0] == '-' || zone[0] == '+')) { zone = zone.substr(0, 3) + ':' + zone.substr(3); } return value + zone; }, toICAL: function(aValue) { var splitzone = vcardValues.time._splitZone(aValue); var zone = splitzone[0], value = splitzone[1]; if (value.length == 8) { value = value.substr(0, 2) + value.substr(3, 2) + value.substr(6, 2); } else if (value.length == 5 && value[0] != '-') { value = value.substr(0, 2) + value.substr(3, 2); } else if (value.length == 6) { value = value.substr(0, 3) + value.substr(4, 2); } if (zone.length == 6 && (zone[0] == '-' || zone[0] == '+')) { zone = zone.substr(0, 3) + zone.substr(4); } return value + zone; }, _splitZone: function(aValue, isFromIcal) { var lastChar = aValue.length - 1; var signChar = aValue.length - (isFromIcal ? 5 : 6); var sign = aValue[signChar]; var zone, value; if (aValue[lastChar] == 'Z') { zone = aValue[lastChar]; value = aValue.substr(0, lastChar); } else if (aValue.length > 6 && (sign == '-' || sign == '+')) { zone = aValue.substr(signChar); value = aValue.substr(0, signChar); } else { zone = ""; value = aValue; } return [zone, value]; } }, "date-time": { decorate: function(aValue) { return ICAL.VCardTime.fromDateAndOrTimeString(aValue, "date-time"); }, undecorate: function(aValue) { return aValue.toString(); }, fromICAL: function(aValue) { return vcardValues['date-and-or-time'].fromICAL(aValue); }, toICAL: function(aValue) { return vcardValues['date-and-or-time'].toICAL(aValue); } }, "date-and-or-time": { decorate: function(aValue) { return ICAL.VCardTime.fromDateAndOrTimeString(aValue, "date-and-or-time"); }, undecorate: function(aValue) { return aValue.toString(); }, fromICAL: function(aValue) { var parts = aValue.split('T'); return (parts[0] ? vcardValues.date.fromICAL(parts[0]) : '') + (parts[1] ? 'T' + vcardValues.time.fromICAL(parts[1]) : ''); }, toICAL: function(aValue) { var parts = aValue.split('T'); return vcardValues.date.toICAL(parts[0]) + (parts[1] ? 'T' + vcardValues.time.toICAL(parts[1]) : ''); } }, timestamp: icalValues['date-time'], "language-tag": { matches: /^[a-zA-Z0-9\-]+$/ // Could go with a more strict regex here } }); var vcardParams = { "type": { valueType: "text", multiValue: "," }, "value": { // since the value here is a 'type' lowercase is used. values: ["text", "uri", "date", "time", "date-time", "date-and-or-time", "timestamp", "boolean", "integer", "float", "utc-offset", "language-tag"], allowXName: true, allowIanaToken: true } }; var vcardProperties = ICAL.helpers.extend(commonProperties, { "adr": { defaultType: "text", structuredValue: ";", multiValue: "," }, "anniversary": DEFAULT_TYPE_DATE_ANDOR_TIME, "bday": DEFAULT_TYPE_DATE_ANDOR_TIME, "caladruri": DEFAULT_TYPE_URI, "caluri": DEFAULT_TYPE_URI, "clientpidmap": DEFAULT_TYPE_TEXT_STRUCTURED, "email": DEFAULT_TYPE_TEXT, "fburl": DEFAULT_TYPE_URI, "fn": DEFAULT_TYPE_TEXT, "gender": DEFAULT_TYPE_TEXT_STRUCTURED, "geo": DEFAULT_TYPE_URI, "impp": DEFAULT_TYPE_URI, "key": DEFAULT_TYPE_URI, "kind": DEFAULT_TYPE_TEXT, "lang": { defaultType: "language-tag" }, "logo": DEFAULT_TYPE_URI, "member": DEFAULT_TYPE_URI, "n": { defaultType: "text", structuredValue: ";", multiValue: "," }, "nickname": DEFAULT_TYPE_TEXT_MULTI, "note": DEFAULT_TYPE_TEXT, "org": { defaultType: "text", structuredValue: ";" }, "photo": DEFAULT_TYPE_URI, "related": DEFAULT_TYPE_URI, "rev": { defaultType: "timestamp" }, "role": DEFAULT_TYPE_TEXT, "sound": DEFAULT_TYPE_URI, "source": DEFAULT_TYPE_URI, "tel": { defaultType: "uri", allowedTypes: ["uri", "text"] }, "title": DEFAULT_TYPE_TEXT, "tz": { defaultType: "text", allowedTypes: ["text", "utc-offset", "uri"] }, "xml": DEFAULT_TYPE_TEXT }); var vcard3Values = ICAL.helpers.extend(commonValues, { binary: icalValues.binary, date: vcardValues.date, "date-time": vcardValues["date-time"], "phone-number": { // TODO /* ... */ }, uri: icalValues.uri, text: icalValues.text, time: icalValues.time, vcard: icalValues.text, "utc-offset": { toICAL: function(aValue) { return aValue.substr(0, 7); }, fromICAL: function(aValue) { return aValue.substr(0, 7); }, decorate: function(aValue) { return ICAL.UtcOffset.fromString(aValue); }, undecorate: function(aValue) { return aValue.toString(); } } }); var vcard3Params = { "type": { valueType: "text", multiValue: "," }, "value": { // since the value here is a 'type' lowercase is used. values: ["text", "uri", "date", "date-time", "phone-number", "time", "boolean", "integer", "float", "utc-offset", "vcard", "binary"], allowXName: true, allowIanaToken: true } }; var vcard3Properties = ICAL.helpers.extend(commonProperties, { fn: DEFAULT_TYPE_TEXT, n: { defaultType: "text", structuredValue: ";", multiValue: "," }, nickname: DEFAULT_TYPE_TEXT_MULTI, photo: { defaultType: "binary", allowedTypes: ["binary", "uri"] }, bday: { defaultType: "date-time", allowedTypes: ["date-time", "date"], detectType: function(string) { return (string.indexOf('T') === -1) ? 'date' : 'date-time'; } }, adr: { defaultType: "text", structuredValue: ";", multiValue: "," }, label: DEFAULT_TYPE_TEXT, tel: { defaultType: "phone-number" }, email: DEFAULT_TYPE_TEXT, mailer: DEFAULT_TYPE_TEXT, tz: { defaultType: "utc-offset", allowedTypes: ["utc-offset", "text"] }, geo: { defaultType: "float", structuredValue: ";" }, title: DEFAULT_TYPE_TEXT, role: DEFAULT_TYPE_TEXT, logo: { defaultType: "binary", allowedTypes: ["binary", "uri"] }, agent: { defaultType: "vcard", allowedTypes: ["vcard", "text", "uri"] }, org: DEFAULT_TYPE_TEXT_STRUCTURED, note: DEFAULT_TYPE_TEXT_MULTI, prodid: DEFAULT_TYPE_TEXT, rev: { defaultType: "date-time", allowedTypes: ["date-time", "date"], detectType: function(string) { return (string.indexOf('T') === -1) ? 'date' : 'date-time'; } }, "sort-string": DEFAULT_TYPE_TEXT, sound: { defaultType: "binary", allowedTypes: ["binary", "uri"] }, class: DEFAULT_TYPE_TEXT, key: { defaultType: "binary", allowedTypes: ["binary", "text"] } }); /** * iCalendar design set * @type {ICAL.design.designSet} */ var icalSet = { value: icalValues, param: icalParams, property: icalProperties }; /** * vCard 4.0 design set * @type {ICAL.design.designSet} */ var vcardSet = { value: vcardValues, param: vcardParams, property: vcardProperties }; /** * vCard 3.0 design set * @type {ICAL.design.designSet} */ var vcard3Set = { value: vcard3Values, param: vcard3Params, property: vcard3Properties }; /** * The design data, used by the parser to determine types for properties and * other metadata needed to produce correct jCard/jCal data. * * @alias ICAL.design * @namespace */ var design = { /** * A designSet describes value, parameter and property data. It is used by * ther parser and stringifier in components and properties to determine they * should be represented. * * @typedef {Object} designSet * @memberOf ICAL.design * @property {Object} value Definitions for value types, keys are type names * @property {Object} param Definitions for params, keys are param names * @property {Object} property Defintions for properties, keys are property names */ /** * The default set for new properties and components if none is specified. * @type {ICAL.design.designSet} */ defaultSet: icalSet, /** * The default type for unknown properties * @type {String} */ defaultType: 'unknown', /** * Holds the design set for known top-level components * * @type {Object} * @property {ICAL.design.designSet} vcard vCard VCARD * @property {ICAL.design.designSet} vevent iCalendar VEVENT * @property {ICAL.design.designSet} vtodo iCalendar VTODO * @property {ICAL.design.designSet} vjournal iCalendar VJOURNAL * @property {ICAL.design.designSet} valarm iCalendar VALARM * @property {ICAL.design.designSet} vtimezone iCalendar VTIMEZONE * @property {ICAL.design.designSet} daylight iCalendar DAYLIGHT * @property {ICAL.design.designSet} standard iCalendar STANDARD * * @example * var propertyName = 'fn'; * var componentDesign = ICAL.design.components.vcard; * var propertyDetails = componentDesign.property[propertyName]; * if (propertyDetails.defaultType == 'text') { * // Yep, sure is... * } */ components: { vcard: vcardSet, vcard3: vcard3Set, vevent: icalSet, vtodo: icalSet, vjournal: icalSet, valarm: icalSet, vtimezone: icalSet, daylight: icalSet, standard: icalSet }, /** * The design set for iCalendar (rfc5545/rfc7265) components. * @type {ICAL.design.designSet} */ icalendar: icalSet, /** * The design set for vCard (rfc6350/rfc7095) components. * @type {ICAL.design.designSet} */ vcard: vcardSet, /** * The design set for vCard (rfc2425/rfc2426/rfc7095) components. * @type {ICAL.design.designSet} */ vcard3: vcard3Set, /** * Gets the design set for the given component name. * * @param {String} componentName The name of the component * @return {ICAL.design.designSet} The design set for the component */ getDesignSet: function(componentName) { var isInDesign = componentName && componentName in design.components; return isInDesign ? design.components[componentName] : design.defaultSet; } }; return design; }()); /* 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-2015 */ /** * Contains various functions to convert jCal and jCard data back into * iCalendar and vCard. * @namespace */ ICAL.stringify = (function() { 'use strict'; var LINE_ENDING = '\r\n'; var DEFAULT_VALUE_TYPE = 'unknown'; var design = ICAL.design; var helpers = ICAL.helpers; /** * Convert a full jCal/jCard array into a iCalendar/vCard string. * * @function ICAL.stringify * @variation function * @param {Array} jCal The jCal/jCard document * @return {String} The stringified iCalendar/vCard document */ function stringify(jCal) { if (typeof jCal[0] == "string") { // This is a single component jCal = [jCal]; } var i = 0; 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. * * @function ICAL.stringify.component * @param {Array} component * jCal/jCard fragment of a component * @param {ICAL.design.designSet} designSet * The design data to use for this component * @return {String} The iCalendar/vCard string */ stringify.component = function(component, designSet) { var name = component[0].toUpperCase(); var result = 'BEGIN:' + name + LINE_ENDING; var props = component[1]; var propIdx = 0; var propLen = props.length; var designSetName = component[0]; // rfc6350 requires that in vCard 4.0 the first component is the VERSION // component with as value 4.0, note that 3.0 does not have this requirement. if (designSetName === 'vcard' && component[1].length > 0 && !(component[1][0][0] === "version" && component[1][0][3] === "4.0")) { designSetName = "vcard3"; } designSet = designSet || design.getDesignSet(designSetName); for (; propIdx < propLen; propIdx++) { result += stringify.property(props[propIdx], designSet) + LINE_ENDING; } var comps = component[2]; var compIdx = 0; var compLen = comps.length; for (; compIdx < compLen; compIdx++) { result += stringify.component(comps[compIdx], designSet) + LINE_ENDING; } result += 'END:' + name; return result; }; /** * Converts a single jCal/jCard property to a iCalendar/vCard string. * * @function ICAL.stringify.property * @param {Array} property * jCal/jCard property array * @param {ICAL.design.designSet} designSet * The design data to use for this property * @param {Boolean} noFold * If true, the line is not folded * @return {String} The iCalendar/vCard string */ stringify.property = function(property, designSet, noFold) { var name = property[0].toUpperCase(); var jsName = property[0]; var params = property[1]; var line = name; var paramName; for (paramName in params) { var value = params[paramName]; /* istanbul ignore else */ if (params.hasOwnProperty(paramName)) { var multiValue = (paramName in designSet.param) && designSet.param[paramName].multiValue; if (multiValue && Array.isArray(value)) { if (designSet.param[paramName].multiValueSeparateDQuote) { multiValue = '"' + multiValue + '"'; } value = value.map(stringify._rfc6868Unescape); value = stringify.multiValue(value, multiValue, "unknown", null, designSet); } else { value = stringify._rfc6868Unescape(value); } line += ';' + paramName.toUpperCase(); line += '=' + stringify.propertyValue(value); } } if (property.length === 3) { // If there are no values, we must assume a blank value return line + ':'; } var valueType = property[2]; if (!designSet) { designSet = design.defaultSet; } var propDetails; var multiValue = false; var structuredValue = false; var isDefault = false; if (jsName in designSet.property) { propDetails = designSet.property[jsName]; if ('multiValue' in propDetails) { multiValue = propDetails.multiValue; } if (('structuredValue' in propDetails) && Array.isArray(property[3])) { structuredValue = propDetails.structuredValue; } if ('defaultType' in propDetails) { if (valueType === propDetails.defaultType) { isDefault = true; } } else { if (valueType === DEFAULT_VALUE_TYPE) { isDefault = true; } } } else { if (valueType === DEFAULT_VALUE_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 && structuredValue) { line += stringify.multiValue( property[3], structuredValue, valueType, multiValue, designSet, structuredValue ); } else if (multiValue) { line += stringify.multiValue( property.slice(3), multiValue, valueType, null, designSet, false ); } else if (structuredValue) { line += stringify.multiValue( property[3], structuredValue, valueType, null, designSet, structuredValue ); } else { line += stringify.value(property[3], valueType, designSet, false); } return noFold ? line : 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. * * @function ICAL.stringify.propertyValue * @param {String} value Raw property 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 ","). * * @function ICAL.stringify.multiValue * @param {Array} values List of values to convert * @param {String} delim Used to join the values (",", ";", ":") * @param {String} type Lowecase ical value type * (like boolean, date-time, etc..) * @param {?String} innerMulti If set, each value will again be processed * Used for structured values * @param {ICAL.design.designSet} designSet * The design data to use for this property * * @return {String} iCalendar/vCard string for value */ stringify.multiValue = function(values, delim, type, innerMulti, designSet, structuredValue) { var result = ''; var len = values.length; var i = 0; for (; i < len; i++) { if (innerMulti && Array.isArray(values[i])) { result += stringify.multiValue(values[i], innerMulti, type, null, designSet, structuredValue); } else { result += stringify.value(values[i], type, designSet, structuredValue); } 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. * * @function ICAL.stringify.value * @param {String|Number} value A formatted value * @param {String} type Lowercase iCalendar/vCard value type * (like boolean, date-time, etc..) * @return {String} iCalendar/vCard value for single value */ stringify.value = function(value, type, designSet, structuredValue) { if (type in designSet.value && 'toICAL' in designSet.value[type]) { return designSet.value[type].toICAL(value, structuredValue); } return value; }; /** * Internal helper for rfc6868. Exposing this on ICAL.stringify so that * hackers can disable the rfc6868 parsing if the really need to. * * @param {String} val The value to unescape * @return {String} The escaped value */ stringify._rfc6868Unescape = function(val) { return val.replace(/[\n^"]/g, function(x) { return RFC6868_REPLACE_MAP[x]; }); }; var RFC6868_REPLACE_MAP = { '"': "^'", "\n": "^n", "^": "^^" }; return stringify; }()); /* 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-2015 */ /** * Contains various functions to parse iCalendar and vCard data. * @namespace */ ICAL.parse = (function() { 'use strict'; var CHAR = /[^ \t]/; var MULTIVALUE_DELIMITER = ','; var VALUE_DELIMITER = ':'; var PARAM_DELIMITER = ';'; var PARAM_NAME_DELIMITER = '='; var DEFAULT_VALUE_TYPE = 'unknown'; var DEFAULT_PARAM_TYPE = 'text'; var design = ICAL.design; var helpers = ICAL.helpers; /** * An error that occurred during parsing. * * @param {String} message The error message * @memberof ICAL.parse * @extends {Error} * @class */ function ParserError(message) { this.message = message; this.name = 'ParserError'; try { throw new Error(); } catch (e) { if (e.stack) { var split = e.stack.split('\n'); split.shift(); this.stack = split.join('\n'); } } } ParserError.prototype = Error.prototype; /** * Parses iCalendar or vCard data into a raw jCal object. Consult * documentation on the {@tutorial layers|layers of parsing} for more * details. * * @function ICAL.parse * @variation function * @todo Fix the API to be more clear on the return type * @param {String} input The string data to parse * @return {Object|Object[]} A single jCal object, or an array thereof */ function parser(input) { var state = {}; var root = state.component = []; state.stack = [root]; parser._eachLine(input, function(err, line) { parser._handleContentLine(line, state); }); // when there are still items on the stack // throw a fatal error, a component was not closed // correctly in that case. if (state.stack.length > 1) { throw new ParserError( 'invalid ical body. component began but did not end' ); } state = null; return (root.length == 1 ? root[0] : root); } /** * Parse an iCalendar property value into the jCal for a single property * * @function ICAL.parse.property * @param {String} str * The iCalendar property string to parse * @param {ICAL.design.designSet=} designSet * The design data to use for this property * @return {Object} * The jCal Object containing the property */ parser.property = function(str, designSet) { var state = { component: [[], []], designSet: designSet || design.defaultSet }; parser._handleContentLine(str, state); return state.component[1][0]; }; /** * Convenience method to parse a component. You can use ICAL.parse() directly * instead. * * @function ICAL.parse.component * @see ICAL.parse(function) * @param {String} str The iCalendar component string to parse * @return {Object} The jCal Object containing the component */ parser.component = function(str) { return parser(str); }; // classes & constants parser.ParserError = ParserError; /** * The state for parsing content lines from an iCalendar/vCard string. * * @private * @memberof ICAL.parse * @typedef {Object} parserState * @property {ICAL.design.designSet} designSet The design set to use for parsing * @property {ICAL.Component[]} stack The stack of components being processed * @property {ICAL.Component} component The currently active component */ /** * Handles a single line of iCalendar/vCard, updating the state. * * @private * @function ICAL.parse._handleContentLine * @param {String} line The content line to process * @param {ICAL.parse.parserState} The current state of the line parsing */ parser._handleContentLine = function(line, state) { // break up the parts of the line var valuePos = line.indexOf(VALUE_DELIMITER); var paramPos = line.indexOf(PARAM_DELIMITER); var lastParamIndex; var lastValuePos; // name of property or begin/end var name; var value; // params is only overridden if paramPos !== -1. // we can't do params = params || {} later on // because it sacrifices ops. var params = {}; /** * Different property cases * * * 1. RRULE:FREQ=foo * // FREQ= is not a param but the value * * 2. ATTENDEE;ROLE=REQ-PARTICIPANT; * // ROLE= is a param because : has not happened yet */ // when the parameter delimiter is after the // value delimiter then its not a parameter. if ((paramPos !== -1 && valuePos !== -1)) { // when the parameter delimiter is after the // value delimiter then its not a parameter. if (paramPos > valuePos) { paramPos = -1; } } var parsedParams; if (paramPos !== -1) { name = line.substring(0, paramPos).toLowerCase(); parsedParams = parser._parseParameters(line.substring(paramPos), 0, state.designSet); if (parsedParams[2] == -1) { throw new ParserError("Invalid parameters in '" + line + "'"); } params = parsedParams[0]; lastParamIndex = parsedParams[1].length + parsedParams[2] + paramPos; if ((lastValuePos = line.substring(lastParamIndex).indexOf(VALUE_DELIMITER)) !== -1) { value = line.substring(lastParamIndex + lastValuePos + 1); } else { throw new ParserError("Missing parameter value in '" + line + "'"); } } else if (valuePos !== -1) { // without parmeters (BEGIN:VCAENDAR, CLASS:PUBLIC) name = line.substring(0, valuePos).toLowerCase(); value = line.substring(valuePos + 1); if (name === 'begin') { var newComponent = [value.toLowerCase(), [], []]; if (state.stack.length === 1) { state.component.push(newComponent); } else { state.component[2].push(newComponent); } state.stack.push(state.component); state.component = newComponent; if (!state.designSet) { state.designSet = design.getDesignSet(state.component[0]); } return; } else if (name === 'end') { state.component = state.stack.pop(); return; } // If its not begin/end, then this is a property with an empty value, // which should be considered valid. } 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 structuredValue = false; var propertyDetails; if (name in state.designSet.property) { propertyDetails = state.designSet.property[name]; if ('multiValue' in propertyDetails) { multiValue = propertyDetails.multiValue; } if ('structuredValue' in propertyDetails) { structuredValue = propertyDetails.structuredValue; } if (value && 'detectType' in propertyDetails) { valueType = propertyDetails.detectType(value); } } // attempt to determine value if (!valueType) { if (!('value' in params)) { if (propertyDetails) { valueType = propertyDetails.defaultType; } else { valueType = DEFAULT_VALUE_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. */ var result; if (multiValue && structuredValue) { value = parser._parseMultiValue(value, structuredValue, valueType, [], multiValue, state.designSet, structuredValue); result = [name, params, valueType, value]; } else if (multiValue) { result = [name, params, valueType]; parser._parseMultiValue(value, multiValue, valueType, result, null, state.designSet, false); } else if (structuredValue) { value = parser._parseMultiValue(value, structuredValue, valueType, [], null, state.designSet, structuredValue); result = [name, params, valueType, value]; } else { value = parser._parseValue(value, valueType, state.designSet, false); result = [name, params, valueType, value]; } // rfc6350 requires that in vCard 4.0 the first component is the VERSION // component with as value 4.0, note that 3.0 does not have this requirement. if (state.component[0] === 'vcard' && state.component[1].length === 0 && !(name === 'version' && value === '4.0')) { state.designSet = design.getDesignSet("vcard3"); } state.component[1].push(result); }; /** * Parse a value from the raw value into the jCard/jCal value. * * @private * @function ICAL.parse._parseValue * @param {String} value Original value * @param {String} type Type of value * @param {Object} designSet The design data to use for this value * @return {Object} varies on type */ parser._parseValue = function(value, type, designSet, structuredValue) { if (type in designSet.value && 'fromICAL' in designSet.value[type]) { return designSet.value[type].fromICAL(value, structuredValue); } return value; }; /** * Parse parameters from a string to object. * * @function ICAL.parse._parseParameters * @private * @param {String} line A single unfolded line * @param {Numeric} start Position to start looking for properties * @param {Object} designSet The design data to use for this property * @return {Object} key/value pairs */ parser._parseParameters = function(line, start, designSet) { var lastParam = start; var pos = 0; var delim = PARAM_NAME_DELIMITER; var result = {}; var name, lcname; var value, valuePos = -1; var type, multiValue; // find the next '=' sign // use lastParam and pos to find name // check if " is used if so get value from "->" // then increment pos to find next ; while ((pos !== false) && (pos = helpers.unescapedIndexOf(line, delim, pos + 1)) !== -1) { name = line.substr(lastParam + 1, pos - lastParam - 1); if (name.length == 0) { throw new ParserError("Empty parameter name in '" + line + "'"); } lcname = name.toLowerCase(); if (lcname in designSet.param && designSet.param[lcname].valueType) { type = designSet.param[lcname].valueType; } else { type = DEFAULT_PARAM_TYPE; } if (lcname in designSet.param) { multiValue = designSet.param[lcname].multiValue; } var nextChar = line[pos + 1]; if (nextChar === '"') { valuePos = pos + 2; pos = helpers.unescapedIndexOf(line, '"', valuePos); if (multiValue && pos !== -1) { var mvpos = pos; var extendValue = true; while (extendValue) { var nextMDelim = helpers.unescapedIndexOf(line, multiValue, mvpos + 1); var nextPDelim = helpers.unescapedIndexOf(line, PARAM_DELIMITER, mvpos + 1); var nextVDelim = helpers.unescapedIndexOf(line, VALUE_DELIMITER, mvpos + 1); var nextDQuote = helpers.unescapedIndexOf(line, '"', mvpos + 1); if (nextDQuote > nextMDelim && (nextPDelim > nextDQuote || nextVDelim > nextDQuote)) { mvpos = helpers.unescapedIndexOf(line, '"', nextDQuote + 1); } else { extendValue = false; } } pos = mvpos; } if (pos === -1) { throw new ParserError( 'invalid line (no matching double quote) "' + line + '"' ); } value = line.substr(valuePos, pos - valuePos); lastParam = helpers.unescapedIndexOf(line, PARAM_DELIMITER, pos); if (lastParam === -1) { pos = false; } } else { valuePos = pos + 1; // move to next ";" var nextPos = helpers.unescapedIndexOf(line, PARAM_DELIMITER, valuePos); var propValuePos = helpers.unescapedIndexOf(line, VALUE_DELIMITER, valuePos); if (propValuePos !== -1 && nextPos > propValuePos) { // this is a delimiter in the property value, let's stop here nextPos = propValuePos; pos = false; } else if (nextPos === -1) { // no ";" if (propValuePos === -1) { nextPos = line.length; } else { nextPos = propValuePos; } pos = false; } else { lastParam = nextPos; pos = nextPos; } value = line.substr(valuePos, nextPos - valuePos); } value = parser._rfc6868Escape(value); if (multiValue) { result[lcname] = parser._parseMultiValue(value, multiValue, type, [], null, designSet); } else { result[lcname] = parser._parseValue(value, type, designSet); } } return [result, value, valuePos]; }; /** * Internal helper for rfc6868. Exposing this on ICAL.parse so that * hackers can disable the rfc6868 parsing if the really need to. * * @function ICAL.parse._rfc6868Escape * @param {String} val The value to escape * @return {String} The escaped value */ parser._rfc6868Escape = function(val) { return val.replace(/\^['n^]/g, function(x) { return RFC6868_REPLACE_MAP[x]; }); }; var RFC6868_REPLACE_MAP = { "^'": '"', "^n": "\n", "^^": "^" }; /** * Parse a multi value string. This function is used either for parsing * actual multi-value property's values, or for handling parameter values. It * can be used for both multi-value properties and structured value properties. * * @private * @function ICAL.parse._parseMultiValue * @param {String} buffer The buffer containing the full value * @param {String} delim The multi-value delimiter * @param {String} type The value type to be parsed * @param {Array.} result The array to append results to, varies on value type * @param {String} innerMulti The inner delimiter to split each value with * @param {ICAL.design.designSet} designSet The design data for this value * @return {?|Array.} Either an array of results, or the first result */ parser._parseMultiValue = function(buffer, delim, type, result, innerMulti, designSet, structuredValue) { var pos = 0; var lastPos = 0; var value; if (delim.length === 0) { return buffer; } // split each piece while ((pos = helpers.unescapedIndexOf(buffer, delim, lastPos)) !== -1) { value = buffer.substr(lastPos, pos - lastPos); if (innerMulti) { value = parser._parseMultiValue(value, innerMulti, type, [], null, designSet, structuredValue); } else { value = parser._parseValue(value, type, designSet, structuredValue); } result.push(value); lastPos = pos + delim.length; } // on the last piece take the rest of string value = buffer.substr(lastPos); if (innerMulti) { value = parser._parseMultiValue(value, innerMulti, type, [], null, designSet, structuredValue); } else { value = parser._parseValue(value, type, designSet, structuredValue); } result.push(value); return result.length == 1 ? result[0] : result; }; /** * Process a complete buffer of iCalendar/vCard data line by line, correctly * unfolding content. Each line will be processed with the given callback * * @private * @function ICAL.parse._eachLine * @param {String} buffer The buffer to process * @param {function(?String, String)} callback The callback for each line */ 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 (pos > 1 && 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 * 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-2015 */ /** * This symbol is further described later on * @ignore */ ICAL.Component = (function() { 'use strict'; var PROPERTY_INDEX = 1; var COMPONENT_INDEX = 2; var NAME_INDEX = 0; /** * @classdesc * Wraps a jCal component, adding convenience methods to add, remove and * update subcomponents and properties. * * @class * @alias ICAL.Component * @param {Array|String} jCal Raw jCal component data OR name of new * component * @param {ICAL.Component} parent Parent component to associate */ function Component(jCal, parent) { if (typeof(jCal) === 'string') { // jCal spec (name, properties, components) jCal = [jCal, [], []]; } // mostly for legacy reasons. this.jCal = jCal; this.parent = parent || null; } Component.prototype = { /** * Hydrated properties are inserted into the _properties array at the same * position as in the jCal array, so its possible the array contains * undefined values for unhydrdated properties. To avoid iterating the * array when checking if all properties have been hydrated, we save the * count here. * * @type {Number} * @private */ _hydratedPropertyCount: 0, /** * The same count as for _hydratedPropertyCount, but for subcomponents * * @type {Number} * @private */ _hydratedComponentCount: 0, /** * The name of this component * @readonly */ get name() { return this.jCal[NAME_INDEX]; }, /** * The design set for this component, e.g. icalendar vs vcard * * @type {ICAL.design.designSet} * @private */ get _designSet() { var parentDesign = this.parent && this.parent._designSet; return parentDesign || ICAL.design.getDesignSet(this.name); }, _hydrateComponent: function(index) { if (!this._components) { this._components = []; this._hydratedComponentCount = 0; } if (this._components[index]) { return this._components[index]; } var comp = new Component( this.jCal[COMPONENT_INDEX][index], this ); this._hydratedComponentCount++; return (this._components[index] = comp); }, _hydrateProperty: function(index) { if (!this._properties) { this._properties = []; this._hydratedPropertyCount = 0; } if (this._properties[index]) { return this._properties[index]; } var prop = new ICAL.Property( this.jCal[PROPERTY_INDEX][index], this ); this._hydratedPropertyCount++; return (this._properties[index] = prop); }, /** * Finds first sub component, optionally filtered by name. * * @param {String=} name Optional name to filter by * @return {?ICAL.Component} The found subcomponent */ 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. * * @param {String=} name Optional name to filter by * @return {ICAL.Component[]} The found sub components */ getAllSubcomponents: function(name) { var jCalLen = this.jCal[COMPONENT_INDEX].length; var i = 0; if (name) { var comps = this.jCal[COMPONENT_INDEX]; var result = []; for (; i < jCalLen; i++) { if (name === comps[i][NAME_INDEX]) { result.push( this._hydrateComponent(i) ); } } return result; } else { if (!this._components || (this._hydratedComponentCount !== jCalLen)) { for (; i < jCalLen; i++) { this._hydrateComponent(i); } } return this._components || []; } }, /** * Returns true when a named property exists. * * @param {String} name The 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 the first property, optionally with the given name. * * @param {String=} name Lowercase property name * @return {?ICAL.Property} The 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 property's value, if available. * * @param {String=} name Lowercase property name * @return {?String} The found property value. */ getFirstPropertyValue: function(name) { var prop = this.getFirstProperty(name); if (prop) { return prop.getFirstValue(); } return null; }, /** * Get all properties in the component, optionally filtered by name. * * @param {String=} name Lowercase property name * @return {ICAL.Property[]} List of properties */ getAllProperties: function(name) { var jCalLen = this.jCal[PROPERTY_INDEX].length; var i = 0; if (name) { var props = this.jCal[PROPERTY_INDEX]; var result = []; for (; i < jCalLen; i++) { if (name === props[i][NAME_INDEX]) { result.push( this._hydrateProperty(i) ); } } return result; } else { if (!this._properties || (this._hydratedPropertyCount !== jCalLen)) { for (; i < jCalLen; i++) { this._hydrateProperty(i); } } return this._properties || []; } }, _removeObjectByIndex: function(jCalIndex, cache, index) { // remove cached version if (cache && cache[index]) { var obj = cache[index]; if ("parent" in obj) { obj.parent = null; } cache.splice(index, 1); } // remove it from the jCal this.jCal[jCalIndex].splice(index, 1); }, _removeObject: function(jCalIndex, cache, nameOrObject) { var i = 0; var objects = this.jCal[jCalIndex]; var len = objects.length; var cached = this[cache]; if (typeof(nameOrObject) === 'string') { for (; i < len; i++) { if (objects[i][NAME_INDEX] === nameOrObject) { this._removeObjectByIndex(jCalIndex, cached, i); return true; } } } else if (cached) { for (; i < len; i++) { if (cached[i] && cached[i] === nameOrObject) { this._removeObjectByIndex(jCalIndex, cached, i); return true; } } } return false; }, _removeAllObjects: function(jCalIndex, cache, name) { var cached = this[cache]; // Unfortunately we have to run through all children to reset their // parent property. var objects = this.jCal[jCalIndex]; var i = objects.length - 1; // descending search required because splice // is used and will effect the indices. for (; i >= 0; i--) { if (!name || objects[i][NAME_INDEX] === name) { this._removeObjectByIndex(jCalIndex, cached, i); } } }, /** * Adds a single sub component. * * @param {ICAL.Component} component The component to add * @return {ICAL.Component} The passed in component */ addSubcomponent: function(component) { if (!this._components) { this._components = []; this._hydratedComponentCount = 0; } if (component.parent) { component.parent.removeSubcomponent(component); } var idx = this.jCal[COMPONENT_INDEX].push(component.jCal); this._components[idx - 1] = component; this._hydratedComponentCount++; component.parent = this; return component; }, /** * Removes a single component by name or the instance of a specific * component. * * @param {ICAL.Component|String} nameOrComp Name of component, or component * @return {Boolean} True when comp is removed */ removeSubcomponent: function(nameOrComp) { var removed = this._removeObject(COMPONENT_INDEX, '_components', nameOrComp); if (removed) { this._hydratedComponentCount--; } return removed; }, /** * Removes all components or (if given) all components by a particular * name. * * @param {String=} name Lowercase component name */ removeAllSubcomponents: function(name) { var removed = this._removeAllObjects(COMPONENT_INDEX, '_components', name); this._hydratedComponentCount = 0; return removed; }, /** * Adds an {@link ICAL.Property} to the component. * * @param {ICAL.Property} property The property to add * @return {ICAL.Property} The passed in property */ addProperty: function(property) { if (!(property instanceof ICAL.Property)) { throw new TypeError('must instance of ICAL.Property'); } if (!this._properties) { this._properties = []; this._hydratedPropertyCount = 0; } if (property.parent) { property.parent.removeProperty(property); } var idx = this.jCal[PROPERTY_INDEX].push(property.jCal); this._properties[idx - 1] = property; this._hydratedPropertyCount++; property.parent = this; return property; }, /** * Helper method to add a property with a value to the component. * * @param {String} name Property name to add * @param {String|Number|Object} value Property value * @return {ICAL.Property} The created property */ addPropertyWithValue: function(name, value) { var prop = new ICAL.Property(name); prop.setValue(value); this.addProperty(prop); return prop; }, /** * Helper method that will update or create a property of the given name * and sets its value. If multiple properties with the given name exist, * only the first is updated. * * @param {String} name Property name to update * @param {String|Number|Object} value Property value * @return {ICAL.Property} The created 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 Property name or instance to remove * @return {Boolean} True, when deleted */ removeProperty: function(nameOrProp) { var removed = this._removeObject(PROPERTY_INDEX, '_properties', nameOrProp); if (removed) { this._hydratedPropertyCount--; } return removed; }, /** * Removes all properties associated with this component, optionally * filtered by name. * * @param {String=} name Lowercase property name * @return {Boolean} True, when deleted */ removeAllProperties: function(name) { var removed = this._removeAllObjects(PROPERTY_INDEX, '_properties', name); this._hydratedPropertyCount = 0; return removed; }, /** * Returns the Object representation of this component. The returned object * is a live jCal object and should be cloned if modified. * @return {Object} */ toJSON: function() { return this.jCal; }, /** * The string representation of this component. * @return {String} */ toString: function() { return ICAL.stringify.component( this.jCal, this._designSet ); } }; /** * Create an {@link ICAL.Component} by parsing the passed iCalendar string. * * @param {String} str The iCalendar string to parse */ Component.fromString = function(str) { return new Component(ICAL.parse.component(str)); }; return Component; }()); /* 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-2015 */ /** * This symbol is further described later on * @ignore */ 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; /** * @classdesc * Provides a layer on top of the raw jCal object for manipulating a single * property, with its parameters and value. * * @description * Its important to note that mutations done in the wrapper * directly 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). * * @class * @alias ICAL.Property * @param {Array|String} jCal Raw jCal representation OR * the new name of the property * * @param {ICAL.Component=} parent Parent component */ function Property(jCal, parent) { this._parent = parent || null; if (typeof(jCal) === 'string') { // We are creating the property by name and need to detect the type this.jCal = [jCal, {}, design.defaultType]; this.jCal[TYPE_INDEX] = this.getDefaultType(); } else { this.jCal = jCal; } this._updateType(); } Property.prototype = { /** * The value type for this property * @readonly * @type {String} */ get type() { return this.jCal[TYPE_INDEX]; }, /** * The name of this property, in lowercase. * @readonly * @type {String} */ get name() { return this.jCal[NAME_INDEX]; }, /** * The parent component for this property. * @type {ICAL.Component} */ get parent() { return this._parent; }, set parent(p) { // Before setting the parent, check if the design set has changed. If it // has, we later need to update the type if it was unknown before. var designSetChanged = !this._parent || (p && p._designSet != this._parent._designSet); this._parent = p; if (this.type == design.defaultType && designSetChanged) { this.jCal[TYPE_INDEX] = this.getDefaultType(); this._updateType(); } return p; }, /** * The design set for this property, e.g. icalendar vs vcard * * @type {ICAL.design.designSet} * @private */ get _designSet() { return this.parent ? this.parent._designSet : design.defaultSet; }, /** * Updates the type metadata from the current jCal type and design set. * * @private */ _updateType: function() { var designSet = this._designSet; if (this.type in designSet.value) { var designType = designSet.value[this.type]; if ('decorate' in designSet.value[this.type]) { this.isDecorated = true; } else { this.isDecorated = false; } if (this.name in designSet.property) { this.isMultiValue = ('multiValue' in designSet.property[this.name]); this.isStructuredValue = ('structuredValue' in designSet.property[this.name]); } } }, /** * Hydrate a single value. The act of hydrating means turning the raw jCal * value into a potentially wrapped object, for example {@link ICAL.Time}. * * @private * @param {Number} index The index of the value to hydrate * @return {Object} The decorated 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 a single value, returning its wrapped object. This is used by * the hydrate function to actually wrap the value. * * @private * @param {?} value The value to decorate * @return {Object} The decorated value */ _decorate: function(value) { return this._designSet.value[this.type].decorate(value, this); }, /** * Undecorate a single value, returning its raw jCal data. * * @private * @param {Object} value The value to undecorate * @return {?} The undecorated value */ _undecorate: function(value) { return this._designSet.value[this.type].undecorate(value, this); }, /** * Sets the value at the given index while also hydrating it. The passed * value can either be a decorated or undecorated value. * * @private * @param {?} value The value to set * @param {Number} index The index to set it at */ _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 parameter on the property. * * @param {String} name Property name (lowercase) * @return {Array|String} Property value */ getParameter: function(name) { if (name in this.jCal[PROP_INDEX]) { return this.jCal[PROP_INDEX][name]; } else { return undefined; } }, /** * Sets a parameter on the property. * * @param {String} name The parameter name * @param {Array|String} value The parameter value */ setParameter: function(name, value) { var lcname = name.toLowerCase(); if (typeof value === "string" && lcname in this._designSet.param && 'multiValue' in this._designSet.param[lcname]) { value = [value]; } this.jCal[PROP_INDEX][name] = value; }, /** * Removes a parameter * * @param {String} name The parameter name */ removeParameter: function(name) { delete this.jCal[PROP_INDEX][name]; }, /** * Get the default type based on this property's name. * * @return {String} The default type for this property */ getDefaultType: function() { var name = this.jCal[NAME_INDEX]; var designSet = this._designSet; if (name in designSet.property) { var details = designSet.property[name]; if ('defaultType' in details) { return details.defaultType; } } return design.defaultType; }, /** * 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 the 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; }, /** * Removes all values from this property */ removeAllValues: function() { if (this._values) { this._values.length = 0; } this.jCal.length = 3; }, /** * Sets the values of the property. Will overwrite the existing values. * This can only be used for multi-value properties. * * @param {Array} values An array of values */ setValues: function(values) { if (!this.isMultiValue) { throw new Error( this.name + ': does not not support mulitValue.\n' + 'override isMultiValue' ); } var len = values.length; var i = 0; this.removeAllValues(); if (len > 0 && typeof(values[0]) === 'object' && 'icaltype' in values[0]) { this.resetType(values[0].icaltype); } if (this.isDecorated) { for (; i < len; i++) { this._setDecoratedValue(values[i], i); } } else { for (; i < len; i++) { this.jCal[VALUE_INDEX + i] = values[i]; } } }, /** * Sets the current value of the property. If this is a multi-value * property, all other values will be removed. * * @param {String|Object} value New property value. */ setValue: function(value) { this.removeAllValues(); if (typeof(value) === 'object' && 'icaltype' in value) { this.resetType(value.icaltype); } if (this.isDecorated) { this._setDecoratedValue(value, 0); } else { this.jCal[VALUE_INDEX] = value; } }, /** * Returns the Object representation of this component. The returned object * is a live jCal object and should be cloned if modified. * @return {Object} */ toJSON: function() { return this.jCal; }, /** * The string representation of this component. * @return {String} */ toICALString: function() { return ICAL.stringify.property( this.jCal, this._designSet, true ); } }; /** * Create an {@link ICAL.Property} by parsing the passed iCalendar string. * * @param {String} str The iCalendar string to parse * @param {ICAL.design.designSet=} designSet The design data to use for this property * @return {ICAL.Property} The created iCalendar property */ Property.fromString = function(str, designSet) { return new Property(ICAL.parse.property(str, designSet)); }; 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-2015 */ /** * This symbol is further described later on * @ignore */ ICAL.UtcOffset = (function() { /** * @classdesc * This class represents the "duration" value type, with various calculation * and manipulation methods. * * @class * @alias ICAL.UtcOffset * @param {Object} aData An object with members of the utc offset * @param {Number=} aData.hours The hours for the utc offset * @param {Number=} aData.minutes The minutes in the utc offset * @param {Number=} aData.factor The factor for the utc-offset, either -1 or 1 */ function UtcOffset(aData) { this.fromData(aData); } UtcOffset.prototype = { /** * The hours in the utc-offset * @type {Number} */ hours: 0, /** * The minutes in the utc-offset * @type {Number} */ minutes: 0, /** * The sign of the utc offset, 1 for positive offset, -1 for negative * offsets. * @type {Number} */ factor: 1, /** * The type name, to be used in the jCal object. * @constant * @type {String} * @default "utc-offset" */ icaltype: "utc-offset", /** * Returns a clone of the utc offset object. * * @return {ICAL.UtcOffset} The cloned object */ clone: function() { return ICAL.UtcOffset.fromSeconds(this.toSeconds()); }, /** * Sets up the current instance using members from the passed data object. * * @param {Object} aData An object with members of the utc offset * @param {Number=} aData.hours The hours for the utc offset * @param {Number=} aData.minutes The minutes in the utc offset * @param {Number=} aData.factor The factor for the utc-offset, either -1 or 1 */ fromData: function(aData) { if (aData) { for (var key in aData) { /* istanbul ignore else */ if (aData.hasOwnProperty(key)) { this[key] = aData[key]; } } } this._normalize(); }, /** * Sets up the current instance from the given seconds value. The seconds * value is truncated to the minute. Offsets are wrapped when the world * ends, the hour after UTC+14:00 is UTC-12:00. * * @param {Number} aSeconds The seconds to convert into an offset */ fromSeconds: function(aSeconds) { var secs = Math.abs(aSeconds); this.factor = aSeconds < 0 ? -1 : 1; this.hours = ICAL.helpers.trunc(secs / 3600); secs -= (this.hours * 3600); this.minutes = ICAL.helpers.trunc(secs / 60); return this; }, /** * Convert the current offset to a value in seconds * * @return {Number} The offset in seconds */ toSeconds: function() { return this.factor * (60 * this.minutes + 3600 * this.hours); }, /** * Compare this utc offset with another one. * * @param {ICAL.UtcOffset} other The other offset to compare with * @return {Number} -1, 0 or 1 for less/equal/greater */ compare: function icaltime_compare(other) { var a = this.toSeconds(); var b = other.toSeconds(); return (a > b) - (b > a); }, _normalize: function() { // Range: 97200 seconds (with 1 hour inbetween) var secs = this.toSeconds(); var factor = this.factor; while (secs < -43200) { // = UTC-12:00 secs += 97200; } while (secs > 50400) { // = UTC+14:00 secs -= 97200; } this.fromSeconds(secs); // Avoid changing the factor when on zero seconds if (secs == 0) { this.factor = factor; } }, /** * The iCalendar string representation of this utc-offset. * @return {String} */ toICALString: function() { return ICAL.design.icalendar.value['utc-offset'].toICAL(this.toString()); }, /** * The string representation of this utc-offset. * @return {String} */ toString: function toString() { return (this.factor == 1 ? "+" : "-") + ICAL.helpers.pad2(this.hours) + ':' + ICAL.helpers.pad2(this.minutes); } }; /** * Creates a new {@link ICAL.UtcOffset} instance from the passed string. * * @param {String} aString The string to parse * @return {ICAL.Duration} The created utc-offset instance */ UtcOffset.fromString = function(aString) { // -05:00 var options = {}; //TODO: support seconds per rfc5545 ? options.factor = (aString[0] === '+') ? 1 : -1; options.hours = ICAL.helpers.strictParseInt(aString.substr(1, 2)); options.minutes = ICAL.helpers.strictParseInt(aString.substr(4, 2)); return new ICAL.UtcOffset(options); }; /** * Creates a new {@link ICAL.UtcOffset} instance from the passed seconds * value. * * @param {Number} aSeconds The number of seconds to convert */ UtcOffset.fromSeconds = function(aSeconds) { var instance = new UtcOffset(); instance.fromSeconds(aSeconds); return instance; }; return UtcOffset; }()); /* 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-2015 */ /** * This symbol is further described later on * @ignore */ ICAL.Binary = (function() { /** * @classdesc * Represents the BINARY value type, which contains extra methods for * encoding and decoding. * * @class * @alias ICAL.Binary * @param {String} aValue The binary data for this value */ function Binary(aValue) { this.value = aValue; } Binary.prototype = { /** * The type name, to be used in the jCal object. * @default "binary" * @constant */ icaltype: "binary", /** * Base64 decode the current value * * @return {String} The base64-decoded value */ decodeValue: function decodeValue() { return this._b64_decode(this.value); }, /** * Encodes the passed parameter with base64 and sets the internal * value to the result. * * @param {String} aValue The raw binary value to encode */ setEncodedValue: function setEncodedValue(aValue) { this.value = this._b64_encode(aValue); }, _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; }, /** * The string representation of this value * @return {String} */ toString: function() { return this.value; } }; /** * Creates a binary value from the given string. * * @param {String} aString The binary value string * @return {ICAL.Binary} The binary value instance */ Binary.fromString = function(aString) { return new Binary(aString); }; return Binary; }()); /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. * Portions Copyright (C) Philipp Kewisch, 2011-2015 */ (function() { /** * @classdesc * This class represents the "period" value type, with various calculation * and manipulation methods. * * @description * The passed data object cannot contain both and end date and a duration. * * @class * @param {Object} aData An object with members of the period * @param {ICAL.Time=} aData.start The start of the period * @param {ICAL.Time=} aData.end The end of the period * @param {ICAL.Duration=} aData.duration The duration of the period */ ICAL.Period = function icalperiod(aData) { this.wrappedJSObject = this; if (aData && 'start' in aData) { if (aData.start && !(aData.start instanceof ICAL.Time)) { throw new TypeError('.start must be an instance of ICAL.Time'); } this.start = aData.start; } if (aData && aData.end && aData.duration) { throw new Error('cannot accept both end and duration'); } if (aData && 'end' in aData) { if (aData.end && !(aData.end instanceof ICAL.Time)) { throw new TypeError('.end must be an instance of ICAL.Time'); } this.end = aData.end; } if (aData && 'duration' in aData) { if (aData.duration && !(aData.duration instanceof ICAL.Duration)) { throw new TypeError('.duration must be an instance of ICAL.Duration'); } this.duration = aData.duration; } }; ICAL.Period.prototype = { /** * The start of the period * @type {ICAL.Time} */ start: null, /** * The end of the period * @type {ICAL.Time} */ end: null, /** * The duration of the period * @type {ICAL.Duration} */ duration: null, /** * The class identifier. * @constant * @type {String} * @default "icalperiod" */ icalclass: "icalperiod", /** * The type name, to be used in the jCal object. * @constant * @type {String} * @default "period" */ icaltype: "period", /** * Returns a clone of the duration object. * * @return {ICAL.Period} The cloned object */ clone: function() { return ICAL.Period.fromData({ start: this.start ? this.start.clone() : null, end: this.end ? this.end.clone() : null, duration: this.duration ? this.duration.clone() : null }); }, /** * Calculates the duration of the period, either directly or by subtracting * start from end date. * * @return {ICAL.Duration} The calculated duration */ getDuration: function duration() { if (this.duration) { return this.duration; } else { return this.end.subtractDate(this.start); } }, /** * Calculates the end date of the period, either directly or by adding * duration to start date. * * @return {ICAL.Time} The calculated end date */ getEnd: function() { if (this.end) { return this.end; } else { var end = this.start.clone(); end.addDuration(this.duration); return end; } }, /** * The string representation of this period. * @return {String} */ toString: function toString() { return this.start + "/" + (this.end || this.duration); }, /** * The jCal representation of this period type. * @return {Object} */ toJSON: function() { return [this.start.toString(), (this.end || this.duration).toString()]; }, /** * The iCalendar string representation of this period. * @return {String} */ toICALString: function() { return this.start.toICALString() + "/" + (this.end || this.duration).toICALString(); } }; /** * Creates a new {@link ICAL.Period} instance from the passed string. * * @param {String} str The string to parse * @param {ICAL.Property} prop The property this period will be on * @return {ICAL.Period} The created period instance */ ICAL.Period.fromString = function fromString(str, prop) { var parts = str.split('/'); if (parts.length !== 2) { throw new Error( 'Invalid string value: "' + str + '" must contain a "/" char.' ); } var options = { start: ICAL.Time.fromDateTimeString(parts[0], prop) }; var end = parts[1]; if (ICAL.Duration.isValueString(end)) { options.duration = ICAL.Duration.fromString(end); } else { options.end = ICAL.Time.fromDateTimeString(end, prop); } return new ICAL.Period(options); }; /** * Creates a new {@link ICAL.Period} instance from the given data object. * The passed data object cannot contain both and end date and a duration. * * @param {Object} aData An object with members of the period * @param {ICAL.Time=} aData.start The start of the period * @param {ICAL.Time=} aData.end The end of the period * @param {ICAL.Duration=} aData.duration The duration of the period * @return {ICAL.Period} The period instance */ ICAL.Period.fromData = function fromData(aData) { return new ICAL.Period(aData); }; /** * Returns a new period instance from the given jCal data array. The first * member is always the start date string, the second member is either a * duration or end date string. * * @param {Array} aData The jCal data array * @param {ICAL.Property} aProp The property this jCal data is on * @return {ICAL.Period} The period instance */ ICAL.Period.fromJSON = function(aData, aProp) { if (ICAL.Duration.isValueString(aData[1])) { return ICAL.Period.fromData({ start: ICAL.Time.fromDateTimeString(aData[0], aProp), duration: ICAL.Duration.fromString(aData[1]) }); } else { return ICAL.Period.fromData({ start: ICAL.Time.fromDateTimeString(aData[0], aProp), end: ICAL.Time.fromDateTimeString(aData[1], aProp) }); } }; })(); /* 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-2015 */ (function() { var DURATION_LETTERS = /([PDWHMTS]{1,1})/; /** * @classdesc * This class represents the "duration" value type, with various calculation * and manipulation methods. * * @class * @alias ICAL.Duration * @param {Object} data An object with members of the duration * @param {Number} data.weeks Duration in weeks * @param {Number} data.days Duration in days * @param {Number} data.hours Duration in hours * @param {Number} data.minutes Duration in minutes * @param {Number} data.seconds Duration in seconds * @param {Boolean} data.isNegative If true, the duration is negative */ ICAL.Duration = function icalduration(data) { this.wrappedJSObject = this; this.fromData(data); }; ICAL.Duration.prototype = { /** * The weeks in this duration * @type {Number} * @default 0 */ weeks: 0, /** * The days in this duration * @type {Number} * @default 0 */ days: 0, /** * The days in this duration * @type {Number} * @default 0 */ hours: 0, /** * The minutes in this duration * @type {Number} * @default 0 */ minutes: 0, /** * The seconds in this duration * @type {Number} * @default 0 */ seconds: 0, /** * The seconds in this duration * @type {Boolean} * @default false */ isNegative: false, /** * The class identifier. * @constant * @type {String} * @default "icalduration" */ icalclass: "icalduration", /** * The type name, to be used in the jCal object. * @constant * @type {String} * @default "duration" */ icaltype: "duration", /** * Returns a clone of the duration object. * * @return {ICAL.Duration} The cloned object */ clone: function clone() { return ICAL.Duration.fromData(this); }, /** * The duration value expressed as a number of seconds. * * @return {Number} The duration value in seconds */ 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); }, /** * Reads the passed seconds value into this duration object. Afterwards, * members like {@link ICAL.Duration#days days} and {@link ICAL.Duration#weeks weeks} will be set up * accordingly. * * @param {Number} aSeconds The duration value in seconds * @return {ICAL.Duration} Returns this instance */ 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; }, /** * Sets up the current instance using members from the passed data object. * * @param {Object} aData An object with members of the duration * @param {Number} aData.weeks Duration in weeks * @param {Number} aData.days Duration in days * @param {Number} aData.hours Duration in hours * @param {Number} aData.minutes Duration in minutes * @param {Number} aData.seconds Duration in seconds * @param {Boolean} aData.isNegative If true, the duration is negative */ fromData: function fromData(aData) { var propsToCopy = ["weeks", "days", "hours", "minutes", "seconds", "isNegative"]; for (var key in propsToCopy) { /* istanbul ignore if */ if (!propsToCopy.hasOwnProperty(key)) { continue; } var prop = propsToCopy[key]; if (aData && prop in aData) { this[prop] = aData[prop]; } else { this[prop] = 0; } } }, /** * Resets the duration instance to the default values, i.e. PT0S */ reset: function reset() { this.isNegative = false; this.weeks = 0; this.days = 0; this.hours = 0; this.minutes = 0; this.seconds = 0; }, /** * Compares the duration instance with another one. * * @param {ICAL.Duration} aOther The instance to compare with * @return {Number} -1, 0 or 1 for less/equal/greater */ compare: function compare(aOther) { var thisSeconds = this.toSeconds(); var otherSeconds = aOther.toSeconds(); return (thisSeconds > otherSeconds) - (thisSeconds < otherSeconds); }, /** * Normalizes the duration instance. For example, a duration with a value * of 61 seconds will be normalized to 1 minute and 1 second. */ normalize: function normalize() { this.fromSeconds(this.toSeconds()); }, /** * The string representation of this duration. * @return {String} */ 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; } }, /** * The iCalendar string representation of this duration. * @return {String} */ toICALString: function() { return this.toString(); } }; /** * Returns a new ICAL.Duration instance from the passed seconds value. * * @param {Number} aSeconds The seconds to create the instance from * @return {ICAL.Duration} The newly created duration instance */ ICAL.Duration.fromSeconds = function icalduration_from_seconds(aSeconds) { return (new ICAL.Duration()).fromSeconds(aSeconds); }; /** * Internal helper function to handle a chunk of a duration. * * @param {String} letter type of duration chunk * @param {String} number numeric value or -/+ * @param {Object} dict target to assign values to */ function parseDurationChunk(letter, number, object) { var type; switch (letter) { case 'P': if (number && number === '-') { object.isNegative = true; } else { object.isNegative = false; } // period break; case 'D': type = 'days'; break; case 'W': type = 'weeks'; break; case 'H': type = 'hours'; break; case 'M': type = 'minutes'; break; case 'S': type = 'seconds'; break; default: // Not a valid chunk return 0; } if (type) { if (!number && number !== 0) { throw new Error( 'invalid duration value: Missing number before "' + letter + '"' ); } var num = parseInt(number, 10); if (ICAL.helpers.isStrictlyNaN(num)) { throw new Error( 'invalid duration value: Invalid number "' + number + '" before "' + letter + '"' ); } object[type] = num; } return 1; } /** * Checks if the given string is an iCalendar duration value. * * @param {String} value The raw ical value * @return {Boolean} True, if the given value is of the * duration ical type */ ICAL.Duration.isValueString = function(string) { return (string[0] === 'P' || string[1] === 'P'); }; /** * Creates a new {@link ICAL.Duration} instance from the passed string. * * @param {String} aStr The string to parse * @return {ICAL.Duration} The created duration instance */ ICAL.Duration.fromString = function icalduration_from_string(aStr) { var pos = 0; var dict = Object.create(null); var chunks = 0; while ((pos = aStr.search(DURATION_LETTERS)) !== -1) { var type = aStr[pos]; var numeric = aStr.substr(0, pos); aStr = aStr.substr(pos + 1); chunks += parseDurationChunk(type, numeric, dict); } if (chunks < 2) { // There must be at least a chunk with "P" and some unit chunk throw new Error( 'invalid duration value: Not enough duration components in "' + aStr + '"' ); } return new ICAL.Duration(dict); }; /** * Creates a new ICAL.Duration instance from the given data object. * * @param {Object} aData An object with members of the duration * @param {Number} aData.weeks Duration in weeks * @param {Number} aData.days Duration in days * @param {Number} aData.hours Duration in hours * @param {Number} aData.minutes Duration in minutes * @param {Number} aData.seconds Duration in seconds * @param {Boolean} aData.isNegative If true, the duration is negative * @return {ICAL.Duration} The createad duration instance */ ICAL.Duration.fromData = function icalduration_from_data(aData) { return new ICAL.Duration(aData); }; })(); /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. * Portions Copyright (C) Philipp Kewisch, 2011-2012 */ (function() { var OPTIONS = ["tzid", "location", "tznames", "latitude", "longitude"]; /** * @classdesc * Timezone representation, created by passing in a tzid and component. * * @example * var vcalendar; * var timezoneComp = vcalendar.getFirstSubcomponent('vtimezone'); * var tzid = timezoneComp.getFirstPropertyValue('tzid'); * * var timezone = new ICAL.Timezone({ * component: timezoneComp, * tzid * }); * * @class * @param {ICAL.Component|Object} data options for class * @param {String|ICAL.Component} data.component * If data is a simple object, then this member can be set to either a * string containing the component data, or an already parsed * ICAL.Component * @param {String} data.tzid The timezone identifier * @param {String} data.location The timezone locationw * @param {String} data.tznames An alternative string representation of the * timezone * @param {Number} data.latitude The latitude of the timezone * @param {Number} data.longitude The longitude of the timezone */ ICAL.Timezone = function icaltimezone(data) { this.wrappedJSObject = this; this.fromData(data); }; ICAL.Timezone.prototype = { /** * Timezone identifier * @type {String} */ tzid: "", /** * Timezone location * @type {String} */ location: "", /** * Alternative timezone name, for the string representation * @type {String} */ tznames: "", /** * The primary latitude for the timezone. * @type {Number} */ latitude: 0.0, /** * The primary longitude for the timezone. * @type {Number} */ longitude: 0.0, /** * The vtimezone component for this timezone. * @type {ICAL.Component} */ component: null, /** * The year this timezone has been expanded to. All timezone transition * dates until this year are known and can be used for calculation * * @private * @type {Number} */ expandedUntilYear: 0, /** * The class identifier. * @constant * @type {String} * @default "icaltimezone" */ icalclass: "icaltimezone", /** * Sets up the current instance using members from the passed data object. * * @param {ICAL.Component|Object} aData options for class * @param {String|ICAL.Component} aData.component * If aData is a simple object, then this member can be set to either a * string containing the component data, or an already parsed * ICAL.Component * @param {String} aData.tzid The timezone identifier * @param {String} aData.location The timezone locationw * @param {String} aData.tznames An alternative string representation of the * timezone * @param {Number} aData.latitude The latitude of the timezone * @param {Number} aData.longitude The longitude of the timezone */ fromData: function fromData(aData) { this.expandedUntilYear = 0; this.changes = []; if (aData instanceof ICAL.Component) { // Either a component is passed directly this.component = aData; } else { // Otherwise the component may be in the data object if (aData && "component" in aData) { if (typeof aData.component == "string") { // If a string was passed, parse it as a component var jCal = ICAL.parse(aData.component); this.component = new ICAL.Component(jCal); } else if (aData.component instanceof ICAL.Component) { // If it was a component already, then just set it this.component = aData.component; } else { // Otherwise just null out the component this.component = null; } } // Copy remaining passed properties for (var key in OPTIONS) { /* istanbul ignore else */ if (OPTIONS.hasOwnProperty(key)) { var prop = OPTIONS[key]; if (aData && prop in aData) { this[prop] = aData[prop]; } } } } // If we have a component but no TZID, attempt to get it from the // component's properties. if (this.component instanceof ICAL.Component && !this.tzid) { this.tzid = this.component.getFirstPropertyValue('tzid'); } return this; }, /** * Finds the utcOffset the given time would occur in this timezone. * * @param {ICAL.Time} tt The time to check for * @return {Number} utc offset in seconds */ utcOffset: function utcOffset(tt) { if (this == ICAL.Timezone.utcTimezone || this == ICAL.Timezone.localTimezone) { return 0; } this._ensureCoverage(tt.year); if (!this.changes.length) { return 0; } var tt_change = { year: tt.year, month: tt.month, day: tt.day, hour: tt.hour, minute: tt.minute, second: tt.second }; var change_num = this._findNearbyChange(tt_change); var change_num_to_use = -1; var step = 1; // TODO: replace with bin search? for (;;) { var change = ICAL.helpers.clone(this.changes[change_num], true); if (change.utcOffset < change.prevUtcOffset) { ICAL.Timezone.adjust_change(change, 0, 0, 0, change.utcOffset); } else { ICAL.Timezone.adjust_change(change, 0, 0, 0, change.prevUtcOffset); } var cmp = ICAL.Timezone._compare_change_fn(tt_change, change); if (cmp >= 0) { change_num_to_use = change_num; } else { step = -1; } if (step == -1 && change_num_to_use != -1) { break; } change_num += step; if (change_num < 0) { return 0; } if (change_num >= this.changes.length) { break; } } var zone_change = this.changes[change_num_to_use]; var utcOffset_change = zone_change.utcOffset - zone_change.prevUtcOffset; if (utcOffset_change < 0 && change_num_to_use > 0) { var tmp_change = ICAL.helpers.clone(zone_change, true); ICAL.Timezone.adjust_change(tmp_change, 0, 0, 0, tmp_change.prevUtcOffset); if (ICAL.Timezone._compare_change_fn(tt_change, tmp_change) < 0) { var prev_zone_change = this.changes[change_num_to_use - 1]; var want_daylight = false; // TODO if (zone_change.is_daylight != want_daylight && prev_zone_change.is_daylight == want_daylight) { zone_change = prev_zone_change; } } } // TODO return is_daylight? return zone_change.utcOffset; }, _findNearbyChange: function icaltimezone_find_nearby_change(change) { // find the closest match var idx = ICAL.helpers.binsearchInsert( this.changes, change, ICAL.Timezone._compare_change_fn ); if (idx >= this.changes.length) { return this.changes.length - 1; } return idx; }, _ensureCoverage: function(aYear) { if (ICAL.Timezone._minimumExpansionYear == -1) { var today = ICAL.Time.now(); ICAL.Timezone._minimumExpansionYear = today.year; } var changesEndYear = aYear; if (changesEndYear < ICAL.Timezone._minimumExpansionYear) { changesEndYear = ICAL.Timezone._minimumExpansionYear; } changesEndYear += ICAL.Timezone.EXTRA_COVERAGE; if (changesEndYear > ICAL.Timezone.MAX_YEAR) { changesEndYear = ICAL.Timezone.MAX_YEAR; } if (!this.changes.length || this.expandedUntilYear < aYear) { var subcomps = this.component.getAllSubcomponents(); var compLen = subcomps.length; var compIdx = 0; for (; compIdx < compLen; compIdx++) { this._expandComponent( subcomps[compIdx], changesEndYear, this.changes ); } this.changes.sort(ICAL.Timezone._compare_change_fn); this.expandedUntilYear = changesEndYear; } }, _expandComponent: function(aComponent, aYear, changes) { if (!aComponent.hasProperty("dtstart") || !aComponent.hasProperty("tzoffsetto") || !aComponent.hasProperty("tzoffsetfrom")) { return null; } var dtstart = aComponent.getFirstProperty("dtstart").getFirstValue(); var change; function convert_tzoffset(offset) { return offset.factor * (offset.hours * 3600 + offset.minutes * 60); } function init_changes() { var changebase = {}; changebase.is_daylight = (aComponent.name == "daylight"); changebase.utcOffset = convert_tzoffset( aComponent.getFirstProperty("tzoffsetto").getFirstValue() ); changebase.prevUtcOffset = convert_tzoffset( aComponent.getFirstProperty("tzoffsetfrom").getFirstValue() ); return changebase; } if (!aComponent.hasProperty("rrule") && !aComponent.hasProperty("rdate")) { change = init_changes(); change.year = dtstart.year; change.month = dtstart.month; change.day = dtstart.day; change.hour = dtstart.hour; change.minute = dtstart.minute; change.second = dtstart.second; ICAL.Timezone.adjust_change(change, 0, 0, 0, -change.prevUtcOffset); changes.push(change); } else { var props = aComponent.getAllProperties("rdate"); for (var rdatekey in props) { /* istanbul ignore if */ if (!props.hasOwnProperty(rdatekey)) { continue; } var rdate = props[rdatekey]; var time = rdate.getFirstValue(); change = init_changes(); change.year = time.year; change.month = time.month; change.day = time.day; if (time.isDate) { change.hour = dtstart.hour; change.minute = dtstart.minute; change.second = dtstart.second; if (dtstart.zone != ICAL.Timezone.utcTimezone) { ICAL.Timezone.adjust_change(change, 0, 0, 0, -change.prevUtcOffset); } } else { change.hour = time.hour; change.minute = time.minute; change.second = time.second; if (time.zone != ICAL.Timezone.utcTimezone) { ICAL.Timezone.adjust_change(change, 0, 0, 0, -change.prevUtcOffset); } } changes.push(change); } var rrule = aComponent.getFirstProperty("rrule"); if (rrule) { rrule = rrule.getFirstValue(); change = init_changes(); if (rrule.until && rrule.until.zone == ICAL.Timezone.utcTimezone) { rrule.until.adjust(0, 0, 0, change.prevUtcOffset); rrule.until.zone = ICAL.Timezone.localTimezone; } var iterator = rrule.iterator(dtstart); var occ; while ((occ = iterator.next())) { change = init_changes(); if (occ.year > aYear || !occ) { break; } change.year = occ.year; change.month = occ.month; change.day = occ.day; change.hour = occ.hour; change.minute = occ.minute; change.second = occ.second; change.isDate = occ.isDate; ICAL.Timezone.adjust_change(change, 0, 0, 0, -change.prevUtcOffset); changes.push(change); } } } return changes; }, /** * The string representation of this timezone. * @return {String} */ toString: function toString() { return (this.tznames ? this.tznames : this.tzid); } }; ICAL.Timezone._compare_change_fn = function icaltimezone_compare_change_fn(a, b) { if (a.year < b.year) return -1; else if (a.year > b.year) return 1; if (a.month < b.month) return -1; else if (a.month > b.month) return 1; if (a.day < b.day) return -1; else if (a.day > b.day) return 1; if (a.hour < b.hour) return -1; else if (a.hour > b.hour) return 1; if (a.minute < b.minute) return -1; else if (a.minute > b.minute) return 1; if (a.second < b.second) return -1; else if (a.second > b.second) return 1; return 0; }; /** * Convert the date/time from one zone to the next. * * @param {ICAL.Time} tt The time to convert * @param {ICAL.Timezone} from_zone The source zone to convert from * @param {ICAL.Timezone} to_zone The target zone to conver to * @return {ICAL.Time} The converted date/time object */ ICAL.Timezone.convert_time = function icaltimezone_convert_time(tt, from_zone, to_zone) { if (tt.isDate || from_zone.tzid == to_zone.tzid || from_zone == ICAL.Timezone.localTimezone || to_zone == ICAL.Timezone.localTimezone) { tt.zone = to_zone; return tt; } var utcOffset = from_zone.utcOffset(tt); tt.adjust(0, 0, 0, - utcOffset); utcOffset = to_zone.utcOffset(tt); tt.adjust(0, 0, 0, utcOffset); return null; }; /** * Creates a new ICAL.Timezone instance from the passed data object. * * @param {ICAL.Component|Object} aData options for class * @param {String|ICAL.Component} aData.component * If aData is a simple object, then this member can be set to either a * string containing the component data, or an already parsed * ICAL.Component * @param {String} aData.tzid The timezone identifier * @param {String} aData.location The timezone locationw * @param {String} aData.tznames An alternative string representation of the * timezone * @param {Number} aData.latitude The latitude of the timezone * @param {Number} aData.longitude The longitude of the timezone */ ICAL.Timezone.fromData = function icaltimezone_fromData(aData) { var tt = new ICAL.Timezone(); return tt.fromData(aData); }; /** * The instance describing the UTC timezone * @type {ICAL.Timezone} * @constant * @instance */ ICAL.Timezone.utcTimezone = ICAL.Timezone.fromData({ tzid: "UTC" }); /** * The instance describing the local timezone * @type {ICAL.Timezone} * @constant * @instance */ ICAL.Timezone.localTimezone = ICAL.Timezone.fromData({ tzid: "floating" }); /** * Adjust a timezone change object. * @private * @param {Object} change The timezone change object * @param {Number} days The extra amount of days * @param {Number} hours The extra amount of hours * @param {Number} minutes The extra amount of minutes * @param {Number} seconds The extra amount of seconds */ ICAL.Timezone.adjust_change = function icaltimezone_adjust_change(change, days, hours, minutes, seconds) { return ICAL.Time.prototype.adjust.call( change, days, hours, minutes, seconds, change ); }; ICAL.Timezone._minimumExpansionYear = -1; ICAL.Timezone.MAX_YEAR = 2035; // TODO this is because of time_t, which we don't need. Still usefull? ICAL.Timezone.EXTRA_COVERAGE = 5; })(); /* 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-2015 */ /** * This symbol is further described later on * @ignore */ ICAL.TimezoneService = (function() { var zones; /** * @classdesc * Singleton class to contain timezones. Right now its all manual registry in * the future we may use this class to download timezone information or handle * loading pre-expanded timezones. * * @namespace * @alias ICAL.TimezoneService */ var TimezoneService = { reset: function() { zones = Object.create(null); var utc = ICAL.Timezone.utcTimezone; zones.Z = utc; zones.UTC = utc; zones.GMT = utc; }, /** * Checks if timezone id has been registered. * * @param {String} tzid Timezone identifier (e.g. America/Los_Angeles) * @return {Boolean} False, when not present */ has: function(tzid) { return !!zones[tzid]; }, /** * Returns a timezone by its tzid if present. * * @param {String} tzid Timezone identifier (e.g. America/Los_Angeles) * @return {?ICAL.Timezone} The timezone, or null if not found */ get: function(tzid) { return zones[tzid]; }, /** * Registers a timezone object or component. * * @param {String=} name * The name of the timezone. Defaults to the component's TZID if not * passed. * @param {ICAL.Component|ICAL.Timezone} zone * The initialized zone or vtimezone. */ register: function(name, timezone) { if (name instanceof ICAL.Component) { if (name.name === 'vtimezone') { timezone = new ICAL.Timezone(name); name = timezone.tzid; } } if (timezone instanceof ICAL.Timezone) { zones[name] = timezone; } else { throw new TypeError('timezone must be ICAL.Timezone or ICAL.Component'); } }, /** * Removes a timezone by its tzid from the list. * * @param {String} tzid Timezone identifier (e.g. America/Los_Angeles) * @return {?ICAL.Timezone} The removed timezone, or null if not registered */ remove: function(tzid) { return (delete zones[tzid]); } }; // initialize defaults TimezoneService.reset(); return TimezoneService; }()); /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. * Portions Copyright (C) Philipp Kewisch, 2011-2015 */ (function() { /** * @classdesc * iCalendar Time representation (similar to JS Date object). Fully * independent of system (OS) timezone / time. Unlike JS Date, the month * January is 1, not zero. * * @example * var time = new ICAL.Time({ * year: 2012, * month: 10, * day: 11 * minute: 0, * second: 0, * isDate: false * }); * * * @alias ICAL.Time * @class * @param {Object} data Time initialization * @param {Number=} data.year The year for this date * @param {Number=} data.month The month for this date * @param {Number=} data.day The day for this date * @param {Number=} data.hour The hour for this date * @param {Number=} data.minute The minute for this date * @param {Number=} data.second The second for this date * @param {Boolean=} data.isDate If true, the instance represents a date (as * opposed to a date-time) * @param {ICAL.Timezone} zone timezone this position occurs in */ ICAL.Time = function icaltime(data, zone) { this.wrappedJSObject = this; var time = this._time = Object.create(null); /* time defaults */ time.year = 0; time.month = 1; time.day = 1; time.hour = 0; time.minute = 0; time.second = 0; time.isDate = false; this.fromData(data, zone); }; ICAL.Time._dowCache = {}; ICAL.Time._wnCache = {}; ICAL.Time.prototype = { /** * The class identifier. * @constant * @type {String} * @default "icaltime" */ icalclass: "icaltime", _cachedUnixTime: null, /** * The type name, to be used in the jCal object. This value may change and * is strictly defined by the {@link ICAL.Time#isDate isDate} member. * @readonly * @type {String} * @default "date-time" */ get icaltype() { return this.isDate ? 'date' : 'date-time'; }, /** * The timezone for this time. * @type {ICAL.Timezone} */ zone: null, /** * Internal uses to indicate that a change has been made and the next read * operation must attempt to normalize the value (for example changing the * day to 33). * * @type {Boolean} * @private */ _pendingNormalization: false, /** * Returns a clone of the time object. * * @return {ICAL.Time} The cloned object */ clone: function() { return new ICAL.Time(this._time, this.zone); }, /** * Reset the time instance to epoch time */ reset: function icaltime_reset() { this.fromData(ICAL.Time.epochTime); this.zone = ICAL.Timezone.utcTimezone; }, /** * Reset the time instance to the given date/time values. * * @param {Number} year The year to set * @param {Number} month The month to set * @param {Number} day The day to set * @param {Number} hour The hour to set * @param {Number} minute The minute to set * @param {Number} second The second to set * @param {ICAL.Timezone} timezone The timezone to set */ 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 }); }, /** * Set up the current instance from the Javascript date value. * * @param {?Date} aDate The Javascript Date to read, or null to reset * @param {Boolean} useUTC If true, the UTC values of the date will be used */ fromJSDate: function icaltime_fromJSDate(aDate, useUTC) { if (!aDate) { this.reset(); } else { if (useUTC) { this.zone = ICAL.Timezone.utcTimezone; this.year = aDate.getUTCFullYear(); this.month = aDate.getUTCMonth() + 1; this.day = aDate.getUTCDate(); this.hour = aDate.getUTCHours(); this.minute = aDate.getUTCMinutes(); this.second = aDate.getUTCSeconds(); } else { this.zone = ICAL.Timezone.localTimezone; this.year = aDate.getFullYear(); this.month = aDate.getMonth() + 1; this.day = aDate.getDate(); this.hour = aDate.getHours(); this.minute = aDate.getMinutes(); this.second = aDate.getSeconds(); } } this._cachedUnixTime = null; return this; }, /** * Sets up the current instance using members from the passed data object. * * @param {Object} aData Time initialization * @param {Number=} aData.year The year for this date * @param {Number=} aData.month The month for this date * @param {Number=} aData.day The day for this date * @param {Number=} aData.hour The hour for this date * @param {Number=} aData.minute The minute for this date * @param {Number=} aData.second The second for this date * @param {Boolean=} aData.isDate If true, the instance represents a date * (as opposed to a date-time) * @param {ICAL.Timezone=} aZone Timezone this position occurs in */ fromData: function fromData(aData, aZone) { if (aData) { for (var key in aData) { /* istanbul ignore else */ if (Object.prototype.hasOwnProperty.call(aData, key)) { // ical type cannot be set if (key === 'icaltype') continue; this[key] = aData[key]; } } } if (aZone) { this.zone = aZone; } if (aData && !("isDate" in aData)) { this.isDate = !("hour" in aData); } else if (aData && ("isDate" in aData)) { this.isDate = aData.isDate; } if (aData && "timezone" in aData) { var zone = ICAL.TimezoneService.get( aData.timezone ); this.zone = zone || ICAL.Timezone.localTimezone; } if (aData && "zone" in aData) { this.zone = aData.zone; } if (!this.zone) { this.zone = ICAL.Timezone.localTimezone; } this._cachedUnixTime = null; return this; }, /** * Calculate the day of week. * @return {ICAL.Time.weekDay} */ dayOfWeek: function icaltime_dayOfWeek() { var dowCacheKey = (this.year << 9) + (this.month << 5) + this.day; if (dowCacheKey in ICAL.Time._dowCache) { return ICAL.Time._dowCache[dowCacheKey]; } // 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)); /* istanbul ignore else */ 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; ICAL.Time._dowCache[dowCacheKey] = h; return h; }, /** * Calculate the day of year. * @return {Number} */ dayOfYear: function dayOfYear() { var is_leap = (ICAL.Time.isLeapYear(this.year) ? 1 : 0); var diypm = ICAL.Time.daysInYearPassedMonth; return diypm[is_leap][this.month - 1] + this.day; }, /** * Returns a copy of the current date/time, rewound to the start of the * week. The resulting ICAL.Time instance is of icaltype date, even if this * is a date-time. * * @param {ICAL.Time.weekDay=} aWeekStart * The week start weekday, defaults to SUNDAY * @return {ICAL.Time} The start of the week (cloned) */ startOfWeek: function startOfWeek(aWeekStart) { var firstDow = aWeekStart || ICAL.Time.SUNDAY; var result = this.clone(); result.day -= ((this.dayOfWeek() + 7 - firstDow) % 7); result.isDate = true; result.hour = 0; result.minute = 0; result.second = 0; return result; }, /** * Returns a copy of the current date/time, shifted to the end of the week. * The resulting ICAL.Time instance is of icaltype date, even if this is a * date-time. * * @param {ICAL.Time.weekDay=} aWeekStart * The week start weekday, defaults to SUNDAY * @return {ICAL.Time} The end of the week (cloned) */ endOfWeek: function endOfWeek(aWeekStart) { var firstDow = aWeekStart || ICAL.Time.SUNDAY; var result = this.clone(); result.day += (7 - this.dayOfWeek() + firstDow - ICAL.Time.SUNDAY) % 7; result.isDate = true; result.hour = 0; result.minute = 0; result.second = 0; return result; }, /** * Returns a copy of the current date/time, rewound to the start of the * month. The resulting ICAL.Time instance is of icaltype date, even if * this is a date-time. * * @return {ICAL.Time} The start of the month (cloned) */ startOfMonth: function startOfMonth() { var result = this.clone(); result.day = 1; result.isDate = true; result.hour = 0; result.minute = 0; result.second = 0; return result; }, /** * Returns a copy of the current date/time, shifted to the end of the * month. The resulting ICAL.Time instance is of icaltype date, even if * this is a date-time. * * @return {ICAL.Time} The end of the month (cloned) */ endOfMonth: function endOfMonth() { var result = this.clone(); result.day = ICAL.Time.daysInMonth(result.month, result.year); result.isDate = true; result.hour = 0; result.minute = 0; result.second = 0; return result; }, /** * Returns a copy of the current date/time, rewound to the start of the * year. The resulting ICAL.Time instance is of icaltype date, even if * this is a date-time. * * @return {ICAL.Time} The start of the year (cloned) */ startOfYear: function startOfYear() { var result = this.clone(); result.day = 1; result.month = 1; result.isDate = true; result.hour = 0; result.minute = 0; result.second = 0; return result; }, /** * Returns a copy of the current date/time, shifted to the end of the * year. The resulting ICAL.Time instance is of icaltype date, even if * this is a date-time. * * @return {ICAL.Time} The end of the year (cloned) */ endOfYear: function endOfYear() { var result = this.clone(); result.day = 31; result.month = 12; result.isDate = true; result.hour = 0; result.minute = 0; result.second = 0; return result; }, /** * First calculates the start of the week, then returns the day of year for * this date. If the day falls into the previous year, the day is zero or negative. * * @param {ICAL.Time.weekDay=} aFirstDayOfWeek * The week start weekday, defaults to SUNDAY * @return {Number} The calculated day of year */ startDoyWeek: function startDoyWeek(aFirstDayOfWeek) { var firstDow = aFirstDayOfWeek || ICAL.Time.SUNDAY; var delta = this.dayOfWeek() - firstDow; if (delta < 0) delta += 7; return this.dayOfYear() - delta; }, /** * Get the dominical letter for the current year. Letters range from A - G * for common years, and AG to GF for leap years. * * @param {Number} yr The year to retrieve the letter for * @return {String} The dominical letter. */ getDominicalLetter: function() { return ICAL.Time.getDominicalLetter(this.year); }, /** * 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 {Number} aDayOfWeek Day of the week see the day name constants * @param {Number} 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 {Number} numeric value indicating a day relative * to the current month of this time object */ nthWeekDay: function icaltime_nthWeekDay(aDayOfWeek, aPos) { var daysInMonth = ICAL.Time.daysInMonth(this.month, this.year); var weekday; var pos = aPos; var start = 0; var otherDay = this.clone(); if (pos >= 0) { otherDay.day = 1; // because 0 means no position has been given // 1 and 0 indicate the same day. if (pos != 0) { // remove the extra numeric value pos--; } // set current start offset to current day. start = otherDay.day; // find the current day of week var startDow = otherDay.dayOfWeek(); // calculate the difference between current // day of the week and desired day of the week var offset = aDayOfWeek - startDow; // if the offset goes into the past // week we add 7 so its goes into the next // week. We only want to go forward in time here. if (offset < 0) // this is really important otherwise we would // end up with dates from in the past. offset += 7; // add offset to start so start is the same // day of the week as the desired day of week. start += offset; // because we are going to add (and multiply) // the numeric value of the day we subtract it // from the start position so not to add it twice. start -= aDayOfWeek; // set week day weekday = aDayOfWeek; } else { // then we set it to the last day in the current month otherDay.day = daysInMonth; // find the ends weekday var endDow = otherDay.dayOfWeek(); pos++; weekday = (endDow - aDayOfWeek); if (weekday < 0) { weekday += 7; } weekday = daysInMonth - weekday; } weekday += pos * 7; return start + weekday; }, /** * Checks if current time is the nth weekday, relative to the current * month. Will always return false when rule resolves outside of current * month. * * @param {ICAL.Time.weekDay} aDayOfWeek Day of week to check * @param {Number} aPos Relative position * @return {Boolean} True, if its the nth weekday */ 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; }, /** * Calculates the ISO 8601 week number. The first week of a year is the * week that contains the first Thursday. The year can have 53 weeks, if * January 1st is a Friday. * * Note there are regions where the first week of the year is the one that * starts on January 1st, which may offset the week number. Also, if a * different week start is specified, this will also affect the week * number. * * @see ICAL.Time.weekOneStarts * @param {ICAL.Time.weekDay} aWeekStart The weekday the week starts with * @return {Number} The ISO week number */ weekNumber: function weekNumber(aWeekStart) { var wnCacheKey = (this.year << 12) + (this.month << 8) + (this.day << 3) + aWeekStart; if (wnCacheKey in ICAL.Time._wnCache) { return ICAL.Time._wnCache[wnCacheKey]; } // This function courtesty of Julian Bucknall, published under the MIT license // http://www.boyet.com/articles/publishedarticles/calculatingtheisoweeknumb.html // plus some fixes to be able to use different week starts. var week1; var dt = this.clone(); dt.isDate = true; var isoyear = this.year; if (dt.month == 12 && dt.day > 25) { week1 = ICAL.Time.weekOneStarts(isoyear + 1, aWeekStart); if (dt.compare(week1) < 0) { week1 = ICAL.Time.weekOneStarts(isoyear, aWeekStart); } else { isoyear++; } } else { week1 = ICAL.Time.weekOneStarts(isoyear, aWeekStart); if (dt.compare(week1) < 0) { week1 = ICAL.Time.weekOneStarts(--isoyear, aWeekStart); } } var daysBetween = (dt.subtractDate(week1).toSeconds() / 86400); var answer = ICAL.helpers.trunc(daysBetween / 7) + 1; ICAL.Time._wnCache[wnCacheKey] = answer; return answer; }, /** * Adds the duration to the current time. The instance is modified in * place. * * @param {ICAL.Duration} aDuration The duration to add */ addDuration: function icaltime_add(aDuration) { var mult = (aDuration.isNegative ? -1 : 1); // because of the duration optimizations it is much // more efficient to grab all the values up front // then set them directly (which will avoid a normalization call). // So we don't actually normalize until we need it. var second = this.second; var minute = this.minute; var hour = this.hour; var day = this.day; second += mult * aDuration.seconds; minute += mult * aDuration.minutes; hour += mult * aDuration.hours; day += mult * aDuration.days; day += mult * 7 * aDuration.weeks; this.second = second; this.minute = minute; this.hour = hour; this.day = day; this._cachedUnixTime = null; }, /** * Subtract the date details (_excluding_ timezone). Useful for finding * the relative difference between two time objects excluding their * timezone differences. * * @param {ICAL.Time} aDate The date to substract * @return {ICAL.Duration} The difference as a duration */ subtractDate: function icaltime_subtract(aDate) { var unixTime = this.toUnixTime() + this.utcOffset(); var other = aDate.toUnixTime() + aDate.utcOffset(); return ICAL.Duration.fromSeconds(unixTime - other); }, /** * Subtract the date details, taking timezones into account. * * @param {ICAL.Time} aDate The date to subtract * @return {ICAL.Duration} The difference in duration */ subtractDateTz: function icaltime_subtract_abs(aDate) { var unixTime = this.toUnixTime(); var other = aDate.toUnixTime(); return ICAL.Duration.fromSeconds(unixTime - other); }, /** * Compares the ICAL.Time instance with another one. * * @param {ICAL.Duration} aOther The instance to compare with * @return {Number} -1, 0 or 1 for less/equal/greater */ compare: function icaltime_compare(other) { var a = this.toUnixTime(); var b = other.toUnixTime(); if (a > b) return 1; if (b > a) return -1; return 0; }, /** * Compares only the date part of this instance with another one. * * @param {ICAL.Duration} other The instance to compare with * @param {ICAL.Timezone} tz The timezone to compare in * @return {Number} -1, 0 or 1 for less/equal/greater */ compareDateOnlyTz: function icaltime_compareDateOnlyTz(other, tz) { function cmp(attr) { return ICAL.Time._cmp_attr(a, b, attr); } var a = this.convertToZone(tz); var b = other.convertToZone(tz); var rc = 0; if ((rc = cmp("year")) != 0) return rc; if ((rc = cmp("month")) != 0) return rc; if ((rc = cmp("day")) != 0) return rc; return rc; }, /** * Convert the instance into another timzone. The returned ICAL.Time * instance is always a copy. * * @param {ICAL.Timezone} zone The zone to convert to * @return {ICAL.Time} The copy, converted to the zone */ convertToZone: function convertToZone(zone) { var copy = this.clone(); var zone_equals = (this.zone.tzid == zone.tzid); if (!this.isDate && !zone_equals) { ICAL.Timezone.convert_time(copy, this.zone, zone); } copy.zone = zone; return copy; }, /** * Calculates the UTC offset of the current date/time in the timezone it is * in. * * @return {Number} UTC offset in seconds */ utcOffset: function utc_offset() { if (this.zone == ICAL.Timezone.localTimezone || this.zone == ICAL.Timezone.utcTimezone) { return 0; } else { return this.zone.utcOffset(this); } }, /** * Returns an RFC 5545 compliant ical representation of this object. * * @return {String} ical date/date-time */ toICALString: function() { var string = this.toString(); if (string.length > 10) { return ICAL.design.icalendar.value['date-time'].toICAL(string); } else { return ICAL.design.icalendar.value.date.toICAL(string); } }, /** * The string representation of this date/time, in jCal form * (including : and - separators). * @return {String} */ toString: function toString() { var result = this.year + '-' + ICAL.helpers.pad2(this.month) + '-' + ICAL.helpers.pad2(this.day); if (!this.isDate) { result += 'T' + ICAL.helpers.pad2(this.hour) + ':' + ICAL.helpers.pad2(this.minute) + ':' + ICAL.helpers.pad2(this.second); if (this.zone === ICAL.Timezone.utcTimezone) { result += 'Z'; } } return result; }, /** * Converts the current instance to a Javascript date * @return {Date} */ toJSDate: function toJSDate() { if (this.zone == ICAL.Timezone.localTimezone) { if (this.isDate) { return new Date(this.year, this.month - 1, this.day); } else { return new Date(this.year, this.month - 1, this.day, this.hour, this.minute, this.second, 0); } } else { return new Date(this.toUnixTime() * 1000); } }, _normalize: function icaltime_normalize() { var isDate = this._time.isDate; if (this._time.isDate) { this._time.hour = 0; this._time.minute = 0; this._time.second = 0; } this.adjust(0, 0, 0, 0); return this; }, /** * Adjust the date/time by the given offset * * @param {Number} aExtraDays The extra amount of days * @param {Number} aExtraHours The extra amount of hours * @param {Number} aExtraMinutes The extra amount of minutes * @param {Number} aExtraSeconds The extra amount of seconds * @param {Number=} aTime The time to adjust, defaults to the * current instance. */ adjust: function icaltime_adjust(aExtraDays, aExtraHours, aExtraMinutes, aExtraSeconds, aTime) { var minutesOverflow, hoursOverflow, daysOverflow = 0, yearsOverflow = 0; var second, minute, hour, day; var daysInMonth; var time = aTime || this._time; if (!time.isDate) { second = time.second + aExtraSeconds; time.second = second % 60; minutesOverflow = ICAL.helpers.trunc(second / 60); if (time.second < 0) { time.second += 60; minutesOverflow--; } minute = time.minute + aExtraMinutes + minutesOverflow; time.minute = minute % 60; hoursOverflow = ICAL.helpers.trunc(minute / 60); if (time.minute < 0) { time.minute += 60; hoursOverflow--; } hour = time.hour + aExtraHours + hoursOverflow; time.hour = hour % 24; daysOverflow = ICAL.helpers.trunc(hour / 24); if (time.hour < 0) { time.hour += 24; daysOverflow--; } } // Adjust month and year first, because we need to know what month the day // is in before adjusting it. if (time.month > 12) { yearsOverflow = ICAL.helpers.trunc((time.month - 1) / 12); } else if (time.month < 1) { yearsOverflow = ICAL.helpers.trunc(time.month / 12) - 1; } time.year += yearsOverflow; time.month -= 12 * yearsOverflow; // Now take care of the days (and adjust month if needed) day = time.day + aExtraDays + daysOverflow; if (day > 0) { for (;;) { daysInMonth = ICAL.Time.daysInMonth(time.month, time.year); if (day <= daysInMonth) { break; } time.month++; if (time.month > 12) { time.year++; time.month = 1; } day -= daysInMonth; } } else { while (day <= 0) { if (time.month == 1) { time.year--; time.month = 12; } else { time.month--; } day += ICAL.Time.daysInMonth(time.month, time.year); } } time.day = day; this._cachedUnixTime = null; return this; }, /** * Sets up the current instance from unix time, the number of seconds since * January 1st, 1970. * * @param {Number} seconds The seconds to set up with */ fromUnixTime: function fromUnixTime(seconds) { this.zone = ICAL.Timezone.utcTimezone; var epoch = ICAL.Time.epochTime.clone(); epoch.adjust(0, 0, 0, seconds); this.year = epoch.year; this.month = epoch.month; this.day = epoch.day; this.hour = epoch.hour; this.minute = epoch.minute; this.second = Math.floor(epoch.second); this._cachedUnixTime = null; }, /** * Converts the current instance to seconds since January 1st 1970. * * @return {Number} Seconds since 1970 */ toUnixTime: function toUnixTime() { if (this._cachedUnixTime !== null) { return this._cachedUnixTime; } var offset = this.utcOffset(); // we use the offset trick to ensure // that we are getting the actual UTC time var ms = Date.UTC( this.year, this.month - 1, this.day, this.hour, this.minute, this.second - offset ); // seconds this._cachedUnixTime = ms / 1000; return this._cachedUnixTime; }, /** * Converts time to into Object which can be serialized then re-created * using the constructor. * * @example * // toJSON will automatically be called * var json = JSON.stringify(mytime); * * var deserialized = JSON.parse(json); * * var time = new ICAL.Time(deserialized); * * @return {Object} */ toJSON: function() { var copy = [ 'year', 'month', 'day', 'hour', 'minute', 'second', 'isDate' ]; var result = Object.create(null); var i = 0; var len = copy.length; var prop; for (; i < len; i++) { prop = copy[i]; result[prop] = this[prop]; } if (this.zone) { result.timezone = this.zone.tzid; } return result; } }; (function setupNormalizeAttributes() { // This needs to run before any instances are created! function defineAttr(attr) { Object.defineProperty(ICAL.Time.prototype, attr, { get: function getTimeAttr() { if (this._pendingNormalization) { this._normalize(); this._pendingNormalization = false; } return this._time[attr]; }, set: function setTimeAttr(val) { this._cachedUnixTime = null; this._pendingNormalization = true; this._time[attr] = val; return val; } }); } /* istanbul ignore else */ if ("defineProperty" in Object) { defineAttr("year"); defineAttr("month"); defineAttr("day"); defineAttr("hour"); defineAttr("minute"); defineAttr("second"); defineAttr("isDate"); } })(); /** * Returns the days in the given month * * @param {Number} month The month to check * @param {Number} year The year to check * @return {Number} The number of days in the month */ ICAL.Time.daysInMonth = function icaltime_daysInMonth(month, year) { var _daysInMonth = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; var days = 30; if (month < 1 || month > 12) return days; days = _daysInMonth[month]; if (month == 2) { days += ICAL.Time.isLeapYear(year); } return days; }; /** * Checks if the year is a leap year * * @param {Number} year The year to check * @return {Boolean} True, if the year is a leap year */ ICAL.Time.isLeapYear = function isLeapYear(year) { if (year <= 1752) { return ((year % 4) == 0); } else { return (((year % 4 == 0) && (year % 100 != 0)) || (year % 400 == 0)); } }; /** * Create a new ICAL.Time from the day of year and year. The date is returned * in floating timezone. * * @param {Number} aDayOfYear The day of year * @param {Number} aYear The year to create the instance in * @return {ICAL.Time} The created instance with the calculated date */ ICAL.Time.fromDayOfYear = function icaltime_fromDayOfYear(aDayOfYear, aYear) { var year = aYear; var doy = aDayOfYear; var tt = new ICAL.Time(); tt.auto_normalize = false; var is_leap = (ICAL.Time.isLeapYear(year) ? 1 : 0); if (doy < 1) { year--; is_leap = (ICAL.Time.isLeapYear(year) ? 1 : 0); doy += ICAL.Time.daysInYearPassedMonth[is_leap][12]; return ICAL.Time.fromDayOfYear(doy, year); } else if (doy > ICAL.Time.daysInYearPassedMonth[is_leap][12]) { is_leap = (ICAL.Time.isLeapYear(year) ? 1 : 0); doy -= ICAL.Time.daysInYearPassedMonth[is_leap][12]; year++; return ICAL.Time.fromDayOfYear(doy, year); } tt.year = year; tt.isDate = true; for (var month = 11; month >= 0; month--) { if (doy > ICAL.Time.daysInYearPassedMonth[is_leap][month]) { tt.month = month + 1; tt.day = doy - ICAL.Time.daysInYearPassedMonth[is_leap][month]; break; } } tt.auto_normalize = true; return tt; }; /** * Returns a new ICAL.Time instance from a date string, e.g 2015-01-02. * * @deprecated Use {@link ICAL.Time.fromDateString} instead * @param {String} str The string to create from * @return {ICAL.Time} The date/time instance */ ICAL.Time.fromStringv2 = function fromString(str) { return new ICAL.Time({ year: parseInt(str.substr(0, 4), 10), month: parseInt(str.substr(5, 2), 10), day: parseInt(str.substr(8, 2), 10), isDate: true }); }; /** * Returns a new ICAL.Time instance from a date string, e.g 2015-01-02. * * @param {String} aValue The string to create from * @return {ICAL.Time} The date/time instance */ ICAL.Time.fromDateString = function(aValue) { // Dates should have no timezone. // Google likes to sometimes specify Z on dates // we specifically ignore that to avoid issues. // YYYY-MM-DD // 2012-10-10 return new ICAL.Time({ year: ICAL.helpers.strictParseInt(aValue.substr(0, 4)), month: ICAL.helpers.strictParseInt(aValue.substr(5, 2)), day: ICAL.helpers.strictParseInt(aValue.substr(8, 2)), isDate: true }); }; /** * Returns a new ICAL.Time instance from a date-time string, e.g * 2015-01-02T03:04:05. If a property is specified, the timezone is set up * from the property's TZID parameter. * * @param {String} aValue The string to create from * @param {ICAL.Property=} prop The property the date belongs to * @return {ICAL.Time} The date/time instance */ ICAL.Time.fromDateTimeString = function(aValue, prop) { if (aValue.length < 19) { throw new Error( 'invalid date-time value: "' + aValue + '"' ); } var zone; if (aValue[19] && aValue[19] === 'Z') { zone = 'Z'; } else if (prop) { zone = prop.getParameter('tzid'); } // 2012-10-10T10:10:10(Z)? var time = new ICAL.Time({ year: ICAL.helpers.strictParseInt(aValue.substr(0, 4)), month: ICAL.helpers.strictParseInt(aValue.substr(5, 2)), day: ICAL.helpers.strictParseInt(aValue.substr(8, 2)), hour: ICAL.helpers.strictParseInt(aValue.substr(11, 2)), minute: ICAL.helpers.strictParseInt(aValue.substr(14, 2)), second: ICAL.helpers.strictParseInt(aValue.substr(17, 2)), timezone: zone }); return time; }; /** * Returns a new ICAL.Time instance from a date or date-time string, * * @param {String} aValue The string to create from * @return {ICAL.Time} The date/time instance */ ICAL.Time.fromString = function fromString(aValue) { if (aValue.length > 10) { return ICAL.Time.fromDateTimeString(aValue); } else { return ICAL.Time.fromDateString(aValue); } }; /** * Creates a new ICAL.Time instance from the given Javascript Date. * * @param {?Date} aDate The Javascript Date to read, or null to reset * @param {Boolean} useUTC If true, the UTC values of the date will be used */ ICAL.Time.fromJSDate = function fromJSDate(aDate, useUTC) { var tt = new ICAL.Time(); return tt.fromJSDate(aDate, useUTC); }; /** * Creates a new ICAL.Time instance from the the passed data object. * * @param {Object} aData Time initialization * @param {Number=} aData.year The year for this date * @param {Number=} aData.month The month for this date * @param {Number=} aData.day The day for this date * @param {Number=} aData.hour The hour for this date * @param {Number=} aData.minute The minute for this date * @param {Number=} aData.second The second for this date * @param {Boolean=} aData.isDate If true, the instance represents a date * (as opposed to a date-time) * @param {ICAL.Timezone=} aZone Timezone this position occurs in */ ICAL.Time.fromData = function fromData(aData, aZone) { var t = new ICAL.Time(); return t.fromData(aData, aZone); }; /** * Creates a new ICAL.Time instance from the current moment. * @return {ICAL.Time} */ ICAL.Time.now = function icaltime_now() { return ICAL.Time.fromJSDate(new Date(), false); }; /** * Returns the date on which ISO week number 1 starts. * * @see ICAL.Time#weekNumber * @param {Number} aYear The year to search in * @param {ICAL.Time.weekDay=} aWeekStart The week start weekday, used for calculation. * @return {ICAL.Time} The date on which week number 1 starts */ ICAL.Time.weekOneStarts = function weekOneStarts(aYear, aWeekStart) { var t = ICAL.Time.fromData({ year: aYear, month: 1, day: 1, isDate: true }); var dow = t.dayOfWeek(); var wkst = aWeekStart || ICAL.Time.DEFAULT_WEEK_START; if (dow > ICAL.Time.THURSDAY) { t.day += 7; } if (wkst > ICAL.Time.THURSDAY) { t.day -= 7; } t.day -= dow - wkst; return t; }; /** * Get the dominical letter for the given year. Letters range from A - G for * common years, and AG to GF for leap years. * * @param {Number} yr The year to retrieve the letter for * @return {String} The dominical letter. */ ICAL.Time.getDominicalLetter = function(yr) { var LTRS = "GFEDCBA"; var dom = (yr + (yr / 4 | 0) + (yr / 400 | 0) - (yr / 100 | 0) - 1) % 7; var isLeap = ICAL.Time.isLeapYear(yr); if (isLeap) { return LTRS[(dom + 6) % 7] + LTRS[dom]; } else { return LTRS[dom]; } }; /** * January 1st, 1970 as an ICAL.Time. * @type {ICAL.Time} * @constant * @instance */ ICAL.Time.epochTime = ICAL.Time.fromData({ year: 1970, month: 1, day: 1, hour: 0, minute: 0, second: 0, isDate: false, timezone: "Z" }); ICAL.Time._cmp_attr = function _cmp_attr(a, b, attr) { if (a[attr] > b[attr]) return 1; if (a[attr] < b[attr]) return -1; return 0; }; /** * The days that have passed in the year after a given month. The array has * two members, one being an array of passed days for non-leap years, the * other analog for leap years. * @example * var isLeapYear = ICAL.Time.isLeapYear(year); * var passedDays = ICAL.Time.daysInYearPassedMonth[isLeapYear][month]; * @type {Array.>} */ ICAL.Time.daysInYearPassedMonth = [ [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] ]; /** * The weekday, 1 = SUNDAY, 7 = SATURDAY. Access via * ICAL.Time.MONDAY, ICAL.Time.TUESDAY, ... * * @typedef {Number} weekDay * @memberof ICAL.Time */ ICAL.Time.SUNDAY = 1; ICAL.Time.MONDAY = 2; ICAL.Time.TUESDAY = 3; ICAL.Time.WEDNESDAY = 4; ICAL.Time.THURSDAY = 5; ICAL.Time.FRIDAY = 6; ICAL.Time.SATURDAY = 7; /** * The default weekday for the WKST part. * @constant * @default ICAL.Time.MONDAY */ ICAL.Time.DEFAULT_WEEK_START = ICAL.Time.MONDAY; })(); /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. * Portions Copyright (C) Philipp Kewisch, 2015 */ (function() { /** * Describes a vCard time, which has slight differences to the ICAL.Time. * Properties can be null if not specified, for example for dates with * reduced accuracy or truncation. * * Note that currently not all methods are correctly re-implemented for * VCardTime. For example, comparison will have undefined results when some * members are null. * * Also, normalization is not yet implemented for this class! * * @alias ICAL.VCardTime * @class * @extends {ICAL.Time} * @param {Object} data The data for the time instance * @param {Number=} data.year The year for this date * @param {Number=} data.month The month for this date * @param {Number=} data.day The day for this date * @param {Number=} data.hour The hour for this date * @param {Number=} data.minute The minute for this date * @param {Number=} data.second The second for this date * @param {ICAL.Timezone|ICAL.UtcOffset} zone The timezone to use * @param {String} icaltype The type for this date/time object */ ICAL.VCardTime = function(data, zone, icaltype) { this.wrappedJSObject = this; var time = this._time = Object.create(null); time.year = null; time.month = null; time.day = null; time.hour = null; time.minute = null; time.second = null; this.icaltype = icaltype || "date-and-or-time"; this.fromData(data, zone); }; ICAL.helpers.inherits(ICAL.Time, ICAL.VCardTime, /** @lends ICAL.VCardTime */ { /** * The class identifier. * @constant * @type {String} * @default "vcardtime" */ icalclass: "vcardtime", /** * The type name, to be used in the jCal object. * @type {String} * @default "date-and-or-time" */ icaltype: "date-and-or-time", /** * The timezone. This can either be floating, UTC, or an instance of * ICAL.UtcOffset. * @type {ICAL.Timezone|ICAL.UtcOFfset} */ zone: null, /** * Returns a clone of the vcard date/time object. * * @return {ICAL.VCardTime} The cloned object */ clone: function() { return new ICAL.VCardTime(this._time, this.zone, this.icaltype); }, _normalize: function() { return this; }, /** * @inheritdoc */ utcOffset: function() { if (this.zone instanceof ICAL.UtcOffset) { return this.zone.toSeconds(); } else { return ICAL.Time.prototype.utcOffset.apply(this, arguments); } }, /** * Returns an RFC 6350 compliant representation of this object. * * @return {String} vcard date/time string */ toICALString: function() { return ICAL.design.vcard.value[this.icaltype].toICAL(this.toString()); }, /** * The string representation of this date/time, in jCard form * (including : and - separators). * @return {String} */ toString: function toString() { var p2 = ICAL.helpers.pad2; var y = this.year, m = this.month, d = this.day; var h = this.hour, mm = this.minute, s = this.second; var hasYear = y !== null, hasMonth = m !== null, hasDay = d !== null; var hasHour = h !== null, hasMinute = mm !== null, hasSecond = s !== null; var datepart = (hasYear ? p2(y) + (hasMonth || hasDay ? '-' : '') : (hasMonth || hasDay ? '--' : '')) + (hasMonth ? p2(m) : '') + (hasDay ? '-' + p2(d) : ''); var timepart = (hasHour ? p2(h) : '-') + (hasHour && hasMinute ? ':' : '') + (hasMinute ? p2(mm) : '') + (!hasHour && !hasMinute ? '-' : '') + (hasMinute && hasSecond ? ':' : '') + (hasSecond ? p2(s) : ''); var zone; if (this.zone === ICAL.Timezone.utcTimezone) { zone = 'Z'; } else if (this.zone instanceof ICAL.UtcOffset) { zone = this.zone.toString(); } else if (this.zone === ICAL.Timezone.localTimezone) { zone = ''; } else if (this.zone instanceof ICAL.Timezone) { var offset = ICAL.UtcOffset.fromSeconds(this.zone.utcOffset(this)); zone = offset.toString(); } else { zone = ''; } switch (this.icaltype) { case "time": return timepart + zone; case "date-and-or-time": case "date-time": return datepart + (timepart == '--' ? '' : 'T' + timepart + zone); case "date": return datepart; } return null; } }); /** * Returns a new ICAL.VCardTime instance from a date and/or time string. * * @param {String} aValue The string to create from * @param {String} aIcalType The type for this instance, e.g. date-and-or-time * @return {ICAL.VCardTime} The date/time instance */ ICAL.VCardTime.fromDateAndOrTimeString = function(aValue, aIcalType) { function part(v, s, e) { return v ? ICAL.helpers.strictParseInt(v.substr(s, e)) : null; } var parts = aValue.split('T'); var dt = parts[0], tmz = parts[1]; var splitzone = tmz ? ICAL.design.vcard.value.time._splitZone(tmz) : []; var zone = splitzone[0], tm = splitzone[1]; var stoi = ICAL.helpers.strictParseInt; var dtlen = dt ? dt.length : 0; var tmlen = tm ? tm.length : 0; var hasDashDate = dt && dt[0] == '-' && dt[1] == '-'; var hasDashTime = tm && tm[0] == '-'; var o = { year: hasDashDate ? null : part(dt, 0, 4), month: hasDashDate && (dtlen == 4 || dtlen == 7) ? part(dt, 2, 2) : dtlen == 7 ? part(dt, 5, 2) : dtlen == 10 ? part(dt, 5, 2) : null, day: dtlen == 5 ? part(dt, 3, 2) : dtlen == 7 && hasDashDate ? part(dt, 5, 2) : dtlen == 10 ? part(dt, 8, 2) : null, hour: hasDashTime ? null : part(tm, 0, 2), minute: hasDashTime && tmlen == 3 ? part(tm, 1, 2) : tmlen > 4 ? hasDashTime ? part(tm, 1, 2) : part(tm, 3, 2) : null, second: tmlen == 4 ? part(tm, 2, 2) : tmlen == 6 ? part(tm, 4, 2) : tmlen == 8 ? part(tm, 6, 2) : null }; if (zone == 'Z') { zone = ICAL.Timezone.utcTimezone; } else if (zone && zone[3] == ':') { zone = ICAL.UtcOffset.fromString(zone); } else { zone = null; } return new ICAL.VCardTime(o, zone, aIcalType); }; })(); /* 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-2015 */ (function() { var DOW_MAP = { SU: ICAL.Time.SUNDAY, MO: ICAL.Time.MONDAY, TU: ICAL.Time.TUESDAY, WE: ICAL.Time.WEDNESDAY, TH: ICAL.Time.THURSDAY, FR: ICAL.Time.FRIDAY, SA: ICAL.Time.SATURDAY }; var REVERSE_DOW_MAP = {}; for (var key in DOW_MAP) { /* istanbul ignore else */ if (DOW_MAP.hasOwnProperty(key)) { REVERSE_DOW_MAP[DOW_MAP[key]] = key; } } var COPY_PARTS = ["BYSECOND", "BYMINUTE", "BYHOUR", "BYDAY", "BYMONTHDAY", "BYYEARDAY", "BYWEEKNO", "BYMONTH", "BYSETPOS"]; /** * @classdesc * This class represents the "recur" value type, with various calculation * and manipulation methods. * * @class * @alias ICAL.Recur * @param {Object} data An object with members of the recurrence * @param {ICAL.Recur.frequencyValues} freq The frequency value * @param {Number=} data.interval The INTERVAL value * @param {ICAL.Time.weekDay=} data.wkst The week start value * @param {ICAL.Time=} data.until The end of the recurrence set * @param {Number=} data.count The number of occurrences * @param {Array.=} data.bysecond The seconds for the BYSECOND part * @param {Array.=} data.byminute The minutes for the BYMINUTE part * @param {Array.=} data.byhour The hours for the BYHOUR part * @param {Array.=} data.byday The BYDAY values * @param {Array.=} data.bymonthday The days for the BYMONTHDAY part * @param {Array.=} data.byyearday The days for the BYYEARDAY part * @param {Array.=} data.byweekno The weeks for the BYWEEKNO part * @param {Array.=} data.bymonth The month for the BYMONTH part * @param {Array.=} data.bysetpos The positionals for the BYSETPOS part */ ICAL.Recur = function icalrecur(data) { this.wrappedJSObject = this; this.parts = {}; if (data && typeof(data) === 'object') { this.fromData(data); } }; ICAL.Recur.prototype = { /** * An object holding the BY-parts of the recurrence rule * @type {Object} */ parts: null, /** * The interval value for the recurrence rule. * @type {Number} */ interval: 1, /** * The week start day * * @type {ICAL.Time.weekDay} * @default ICAL.Time.MONDAY */ wkst: ICAL.Time.MONDAY, /** * The end of the recurrence * @type {?ICAL.Time} */ until: null, /** * The maximum number of occurrences * @type {?Number} */ count: null, /** * The frequency value. * @type {ICAL.Recur.frequencyValues} */ freq: null, /** * The class identifier. * @constant * @type {String} * @default "icalrecur" */ icalclass: "icalrecur", /** * The type name, to be used in the jCal object. * @constant * @type {String} * @default "recur" */ icaltype: "recur", /** * Create a new iterator for this recurrence rule. The passed start date * must be the start date of the event, not the start of the range to * search in. * * @example * var recur = comp.getFirstPropertyValue('rrule'); * var dtstart = comp.getFirstPropertyValue('dtstart'); * var iter = recur.iterator(dtstart); * for (var next = iter.next(); next; next = iter.next()) { * if (next.compare(rangeStart) < 0) { * continue; * } * console.log(next.toString()); * } * * @param {ICAL.Time} aStart The item's start date * @return {ICAL.RecurIterator} The recurrence iterator */ iterator: function(aStart) { return new ICAL.RecurIterator({ rule: this, dtstart: aStart }); }, /** * Returns a clone of the recurrence object. * * @return {ICAL.Recur} The cloned object */ clone: function clone() { return new ICAL.Recur(this.toJSON()); }, /** * Checks if the current rule is finite, i.e. has a count or until part. * * @return {Boolean} True, if the rule is finite */ isFinite: function isfinite() { return !!(this.count || this.until); }, /** * Checks if the current rule has a count part, and not limited by an until * part. * * @return {Boolean} True, if the rule is by count */ isByCount: function isbycount() { return !!(this.count && !this.until); }, /** * Adds a component (part) to the recurrence rule. This is not a component * in the sense of {@link ICAL.Component}, but a part of the recurrence * rule, i.e. BYMONTH. * * @param {String} aType The name of the component part * @param {Array|String} aValue The component value */ addComponent: function addPart(aType, aValue) { var ucname = aType.toUpperCase(); if (ucname in this.parts) { this.parts[ucname].push(aValue); } else { this.parts[ucname] = [aValue]; } }, /** * Sets the component value for the given by-part. * * @param {String} aType The component part name * @param {Array} aValues The component values */ setComponent: function setComponent(aType, aValues) { this.parts[aType.toUpperCase()] = aValues.slice(); }, /** * Gets (a copy) of the requested component value. * * @param {String} aType The component part name * @return {Array} The component part value */ getComponent: function getComponent(aType) { var ucname = aType.toUpperCase(); return (ucname in this.parts ? this.parts[ucname].slice() : []); }, /** * Retrieves the next occurrence after the given recurrence id. See the * guide on {@tutorial terminology} for more details. * * NOTE: Currently, this method iterates all occurrences from the start * date. It should not be called in a loop for performance reasons. If you * would like to get more than one occurrence, you can iterate the * occurrences manually, see the example on the * {@link ICAL.Recur#iterator iterator} method. * * @param {ICAL.Time} aStartTime The start of the event series * @param {ICAL.Time} aRecurrenceId The date of the last occurrence * @return {ICAL.Time} The next occurrence after */ getNextOccurrence: function getNextOccurrence(aStartTime, aRecurrenceId) { var iter = this.iterator(aStartTime); var next, cdt; do { next = iter.next(); } while (next && next.compare(aRecurrenceId) <= 0); if (next && aRecurrenceId.zone) { next.zone = aRecurrenceId.zone; } return next; }, /** * Sets up the current instance using members from the passed data object. * * @param {Object} data An object with members of the recurrence * @param {ICAL.Recur.frequencyValues} freq The frequency value * @param {Number=} data.interval The INTERVAL value * @param {ICAL.Time.weekDay=} data.wkst The week start value * @param {ICAL.Time=} data.until The end of the recurrence set * @param {Number=} data.count The number of occurrences * @param {Array.=} data.bysecond The seconds for the BYSECOND part * @param {Array.=} data.byminute The minutes for the BYMINUTE part * @param {Array.=} data.byhour The hours for the BYHOUR part * @param {Array.=} data.byday The BYDAY values * @param {Array.=} data.bymonthday The days for the BYMONTHDAY part * @param {Array.=} data.byyearday The days for the BYYEARDAY part * @param {Array.=} data.byweekno The weeks for the BYWEEKNO part * @param {Array.=} data.bymonth The month for the BYMONTH part * @param {Array.=} data.bysetpos The positionals for the BYSETPOS part */ fromData: function(data) { for (var key in data) { var uckey = key.toUpperCase(); if (uckey in partDesign) { if (Array.isArray(data[key])) { this.parts[uckey] = data[key]; } else { this.parts[uckey] = [data[key]]; } } else { this[key] = data[key]; } } if (this.wkst && typeof this.wkst != "number") { this.wkst = ICAL.Recur.icalDayToNumericDay(this.wkst); } if (this.until && !(this.until instanceof ICAL.Time)) { this.until = ICAL.Time.fromString(this.until); } }, /** * The jCal representation of this recurrence type. * @return {Object} */ toJSON: function() { var res = Object.create(null); res.freq = this.freq; if (this.count) { res.count = this.count; } if (this.interval > 1) { res.interval = this.interval; } for (var k in this.parts) { /* istanbul ignore if */ if (!this.parts.hasOwnProperty(k)) { continue; } var kparts = this.parts[k]; if (Array.isArray(kparts) && kparts.length == 1) { res[k.toLowerCase()] = kparts[0]; } else { res[k.toLowerCase()] = ICAL.helpers.clone(this.parts[k]); } } if (this.until) { res.until = this.until.toString(); } if ('wkst' in this && this.wkst !== ICAL.Time.DEFAULT_WEEK_START) { res.wkst = ICAL.Recur.numericDayToIcalDay(this.wkst); } return res; }, /** * The string representation of this recurrence rule. * @return {String} */ 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) { /* istanbul ignore else */ if (this.parts.hasOwnProperty(k)) { str += ";" + k + "=" + this.parts[k]; } } if (this.until) { str += ';UNTIL=' + this.until.toString(); } if ('wkst' in this && this.wkst !== ICAL.Time.DEFAULT_WEEK_START) { str += ';WKST=' + ICAL.Recur.numericDayToIcalDay(this.wkst); } return str; } }; function parseNumericValue(type, min, max, value) { var result = value; if (value[0] === '+') { result = value.substr(1); } result = ICAL.helpers.strictParseInt(result); if (min !== undefined && value < min) { throw new Error( type + ': invalid value "' + value + '" must be > ' + min ); } if (max !== undefined && value > max) { throw new Error( type + ': invalid value "' + value + '" must be < ' + min ); } return result; } /** * Convert an ical representation of a day (SU, MO, etc..) * into a numeric value of that day. * * @param {String} string The iCalendar day name * @return {Number} Numeric value of given day */ ICAL.Recur.icalDayToNumericDay = function toNumericDay(string) { //XXX: this is here so we can deal // with possibly invalid string values. return DOW_MAP[string]; }; /** * Convert a numeric day value into its ical representation (SU, MO, etc..) * * @param {Number} num Numeric value of given day * @return {String} The ICAL day value, e.g SU,MO,... */ ICAL.Recur.numericDayToIcalDay = function toIcalDay(num) { //XXX: this is here so we can deal with possibly invalid number values. // Also, this allows consistent mapping between day numbers and day // names for external users. return REVERSE_DOW_MAP[num]; }; var VALID_DAY_NAMES = /^(SU|MO|TU|WE|TH|FR|SA)$/; var VALID_BYDAY_PART = /^([+-])?(5[0-3]|[1-4][0-9]|[1-9])?(SU|MO|TU|WE|TH|FR|SA)$/; /** * Possible frequency values for the FREQ part * (YEARLY, MONTHLY, WEEKLY, DAILY, HOURLY, MINUTELY, SECONDLY) * * @typedef {String} frequencyValues * @memberof ICAL.Recur */ var ALLOWED_FREQ = ['SECONDLY', 'MINUTELY', 'HOURLY', 'DAILY', 'WEEKLY', 'MONTHLY', 'YEARLY']; var optionDesign = { FREQ: function(value, dict, fmtIcal) { // yes this is actually equal or faster then regex. // upside here is we can enumerate the valid values. if (ALLOWED_FREQ.indexOf(value) !== -1) { dict.freq = value; } else { throw new Error( 'invalid frequency "' + value + '" expected: "' + ALLOWED_FREQ.join(', ') + '"' ); } }, COUNT: function(value, dict, fmtIcal) { dict.count = ICAL.helpers.strictParseInt(value); }, INTERVAL: function(value, dict, fmtIcal) { dict.interval = ICAL.helpers.strictParseInt(value); if (dict.interval < 1) { // 0 or negative values are not allowed, some engines seem to generate // it though. Assume 1 instead. dict.interval = 1; } }, UNTIL: function(value, dict, fmtIcal) { if (fmtIcal) { if (value.length > 10) { dict.until = ICAL.design.icalendar.value['date-time'].fromICAL(value); } else { dict.until = ICAL.design.icalendar.value.date.fromICAL(value); } } else { dict.until = ICAL.Time.fromString(value); } }, WKST: function(value, dict, fmtIcal) { if (VALID_DAY_NAMES.test(value)) { dict.wkst = ICAL.Recur.icalDayToNumericDay(value); } else { throw new Error('invalid WKST value "' + value + '"'); } } }; var partDesign = { BYSECOND: parseNumericValue.bind(this, 'BYSECOND', 0, 60), BYMINUTE: parseNumericValue.bind(this, 'BYMINUTE', 0, 59), BYHOUR: parseNumericValue.bind(this, 'BYHOUR', 0, 23), BYDAY: function(value) { if (VALID_BYDAY_PART.test(value)) { return value; } else { throw new Error('invalid BYDAY value "' + value + '"'); } }, BYMONTHDAY: parseNumericValue.bind(this, 'BYMONTHDAY', -31, 31), BYYEARDAY: parseNumericValue.bind(this, 'BYYEARDAY', -366, 366), BYWEEKNO: parseNumericValue.bind(this, 'BYWEEKNO', -53, 53), BYMONTH: parseNumericValue.bind(this, 'BYMONTH', 0, 12), BYSETPOS: parseNumericValue.bind(this, 'BYSETPOS', -366, 366) }; /** * Creates a new {@link ICAL.Recur} instance from the passed string. * * @param {String} string The string to parse * @return {ICAL.Recur} The created recurrence instance */ ICAL.Recur.fromString = function(string) { var data = ICAL.Recur._stringToData(string, false); return new ICAL.Recur(data); }; /** * Creates a new {@link ICAL.Recur} instance using members from the passed * data object. * * @param {Object} aData An object with members of the recurrence * @param {ICAL.Recur.frequencyValues} freq The frequency value * @param {Number=} aData.interval The INTERVAL value * @param {ICAL.Time.weekDay=} aData.wkst The week start value * @param {ICAL.Time=} aData.until The end of the recurrence set * @param {Number=} aData.count The number of occurrences * @param {Array.=} aData.bysecond The seconds for the BYSECOND part * @param {Array.=} aData.byminute The minutes for the BYMINUTE part * @param {Array.=} aData.byhour The hours for the BYHOUR part * @param {Array.=} aData.byday The BYDAY values * @param {Array.=} aData.bymonthday The days for the BYMONTHDAY part * @param {Array.=} aData.byyearday The days for the BYYEARDAY part * @param {Array.=} aData.byweekno The weeks for the BYWEEKNO part * @param {Array.=} aData.bymonth The month for the BYMONTH part * @param {Array.=} aData.bysetpos The positionals for the BYSETPOS part */ ICAL.Recur.fromData = function(aData) { return new ICAL.Recur(aData); }; /** * Converts a recurrence string to a data object, suitable for the fromData * method. * * @param {String} string The string to parse * @param {Boolean} fmtIcal If true, the string is considered to be an * iCalendar string * @return {ICAL.Recur} The recurrence instance */ ICAL.Recur._stringToData = function(string, fmtIcal) { var dict = Object.create(null); // split is slower in FF but fast enough. // v8 however this is faster then manual split? var values = string.split(';'); var len = values.length; for (var i = 0; i < len; i++) { var parts = values[i].split('='); var ucname = parts[0].toUpperCase(); var lcname = parts[0].toLowerCase(); var name = (fmtIcal ? lcname : ucname); var value = parts[1]; if (ucname in partDesign) { var partArr = value.split(','); var partArrIdx = 0; var partArrLen = partArr.length; for (; partArrIdx < partArrLen; partArrIdx++) { partArr[partArrIdx] = partDesign[ucname](partArr[partArrIdx]); } dict[name] = (partArr.length == 1 ? partArr[0] : partArr); } else if (ucname in optionDesign) { optionDesign[ucname](value, dict, fmtIcal); } else { // Don't swallow unknown values. Just set them as they are. dict[lcname] = value; } } return dict; }; })(); /* 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-2015 */ /** * This symbol is further described later on * @ignore */ ICAL.RecurIterator = (function() { /** * @classdesc * An iterator for a single recurrence rule. This class usually doesn't have * to be instanciated directly, the convenience method * {@link ICAL.Recur#iterator} can be used. * * @description * The options object may contain additional members when resuming iteration from a previous run * * @description * The options object may contain additional members when resuming iteration * from a previous run. * * @class * @alias ICAL.RecurIterator * @param {Object} options The iterator options * @param {ICAL.Recur} options.rule The rule to iterate. * @param {ICAL.Time} options.dtstart The start date of the event. * @param {Boolean=} options.initialized When true, assume that options are * from a previously constructed iterator. Initialization will not be * repeated. */ function icalrecur_iterator(options) { this.fromData(options); } icalrecur_iterator.prototype = { /** * True when iteration is finished. * @type {Boolean} */ completed: false, /** * The rule that is being iterated * @type {ICAL.Recur} */ rule: null, /** * The start date of the event being iterated. * @type {ICAL.Time} */ dtstart: null, /** * The last occurrence that was returned from the * {@link ICAL.RecurIterator#next} method. * @type {ICAL.Time} */ last: null, /** * The sequence number from the occurrence * @type {Number} */ occurrence_number: 0, /** * The indices used for the {@link ICAL.RecurIterator#by_data} object. * @type {Object} * @private */ by_indices: null, /** * If true, the iterator has already been initialized * @type {Boolean} * @private */ initialized: false, /** * The initializd by-data. * @type {Object} * @private */ by_data: null, /** * The expanded yeardays * @type {Array} * @private */ days: null, /** * The index in the {@link ICAL.RecurIterator#days} array. * @type {Number} * @private */ days_index: 0, /** * Initialize the recurrence iterator from the passed data object. This * method is usually not called directly, you can initialize the iterator * through the constructor. * * @param {Object} options The iterator options * @param {ICAL.Recur} options.rule The rule to iterate. * @param {ICAL.Time} options.dtstart The start date of the event. * @param {Boolean=} options.initialized When true, assume that options are * from a previously constructed iterator. Initialization will not be * repeated. */ fromData: function(options) { this.rule = ICAL.helpers.formatClassType(options.rule, ICAL.Recur); if (!this.rule) { throw new Error('iterator requires a (ICAL.Recur) rule'); } this.dtstart = ICAL.helpers.formatClassType(options.dtstart, ICAL.Time); if (!this.dtstart) { throw new Error('iterator requires a (ICAL.Time) dtstart'); } if (options.by_data) { this.by_data = options.by_data; } else { this.by_data = ICAL.helpers.clone(this.rule.parts, true); } if (options.occurrence_number) this.occurrence_number = options.occurrence_number; this.days = options.days || []; if (options.last) { this.last = ICAL.helpers.formatClassType(options.last, ICAL.Time); } this.by_indices = options.by_indices; if (!this.by_indices) { this.by_indices = { "BYSECOND": 0, "BYMINUTE": 0, "BYHOUR": 0, "BYDAY": 0, "BYMONTH": 0, "BYWEEKNO": 0, "BYMONTHDAY": 0 }; } this.initialized = options.initialized || false; if (!this.initialized) { this.init(); } }, /** * Intialize the iterator * @private */ 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 bydayParts = this.ruleDayOfWeek(parts.BYDAY[0]); var pos = bydayParts[0]; var dow = bydayParts[1]; var wkdy = dow - this.last.dayOfWeek(); if ((this.last.dayOfWeek() < dow && wkdy >= 0) || wkdy < 0) { // Initial time is after first day of BYDAY data this.last.day += wkdy; } } else { var dayName = ICAL.Recur.numericDayToIcalDay(this.dtstart.dayOfWeek()); parts.BYDAY = [dayName]; } } if (this.rule.freq == "YEARLY") { for (;;) { this.expand_year_days(this.last.year); if (this.days.length > 0) { break; } this.increment_year(this.rule.interval); } this._nextByYearDay(); } if (this.rule.freq == "MONTHLY" && this.has_by_data("BYDAY")) { var tempLast = null; var initLast = this.last.clone(); var daysInMonth = ICAL.Time.daysInMonth(this.last.month, this.last.year); // Check every weekday in BYDAY with relative dow and pos. for (var i in this.by_data.BYDAY) { /* istanbul ignore if */ if (!this.by_data.BYDAY.hasOwnProperty(i)) { continue; } this.last = initLast.clone(); var bydayParts = this.ruleDayOfWeek(this.by_data.BYDAY[i]); var pos = bydayParts[0]; var dow = bydayParts[1]; var dayOfMonth = this.last.nthWeekDay(dow, pos); // If |pos| >= 6, the byday is invalid for a monthly rule. if (pos >= 6 || pos <= -6) { throw new Error("Malformed values in BYDAY part"); } // If a Byday with pos=+/-5 is not in the current month it // must be searched in the next months. if (dayOfMonth > daysInMonth || dayOfMonth <= 0) { // Skip if we have already found a "last" in this month. if (tempLast && tempLast.month == initLast.month) { continue; } while (dayOfMonth > daysInMonth || dayOfMonth <= 0) { this.increment_month(); daysInMonth = ICAL.Time.daysInMonth(this.last.month, this.last.year); dayOfMonth = this.last.nthWeekDay(dow, pos); } } this.last.day = dayOfMonth; if (!tempLast || this.last.compare(tempLast) < 0) { tempLast = this.last.clone(); } } this.last = tempLast.clone(); //XXX: This feels like a hack, but we need to initialize // the BYMONTHDAY case correctly and byDayAndMonthDay handles // this case. It accepts a special flag which will avoid incrementing // the initial value without the flag days that match the start time // would be missed. if (this.has_by_data('BYMONTHDAY')) { this._byDayAndMonthDay(true); } if (this.last.day > daysInMonth || this.last.day == 0) { throw new Error("Malformed values in BYDAY part"); } } else if (this.has_by_data("BYMONTHDAY")) { if (this.last.day < 0) { var daysInMonth = ICAL.Time.daysInMonth(this.last.month, this.last.year); this.last.day = daysInMonth + this.last.day + 1; } } }, /** * Retrieve the next occurrence from the iterator. * @return {ICAL.Time} */ 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; } var valid; do { valid = 1; switch (this.rule.freq) { case "SECONDLY": this.next_second(); break; case "MINUTELY": this.next_minute(); break; case "HOURLY": this.next_hour(); break; case "DAILY": this.next_day(); break; case "WEEKLY": this.next_week(); break; case "MONTHLY": valid = this.next_month(); break; case "YEARLY": this.next_year(); break; default: return null; } } while (!this.check_contracting_rules() || this.last.compare(this.dtstart) < 0 || !valid); // TODO is this valid? if (this.last.compare(before) == 0) { throw new Error("Same occurrence found twice, protecting " + "you from death by recursion"); } if (this.rule.until && this.last.compare(this.rule.until) > 0) { this.completed = true; return null; } else { this.occurrence_number++; return this.last; } }, next_second: function next_second() { return this.next_generic("BYSECOND", "SECONDLY", "second", "minute"); }, increment_second: function increment_second(inc) { return this.increment_generic(inc, "second", 60, "minute"); }, next_minute: function next_minute() { return this.next_generic("BYMINUTE", "MINUTELY", "minute", "hour", "next_second"); }, increment_minute: function increment_minute(inc) { return this.increment_generic(inc, "minute", 60, "hour"); }, next_hour: function next_hour() { return this.next_generic("BYHOUR", "HOURLY", "hour", "monthday", "next_minute"); }, increment_hour: function increment_hour(inc) { this.increment_generic(inc, "hour", 24, "monthday"); }, next_day: function next_day() { var has_by_day = ("BYDAY" in this.by_data); var this_freq = (this.rule.freq == "DAILY"); if (this.next_hour() == 0) { return 0; } if (this_freq) { this.increment_monthday(this.rule.interval); } else { this.increment_monthday(1); } return 0; }, next_week: function next_week() { var end_of_data = 0; if (this.next_weekday_by_week() == 0) { return end_of_data; } if (this.has_by_data("BYWEEKNO")) { var idx = ++this.by_indices.BYWEEKNO; if (this.by_indices.BYWEEKNO == this.by_data.BYWEEKNO.length) { this.by_indices.BYWEEKNO = 0; end_of_data = 1; } // HACK should be first month of the year this.last.month = 1; this.last.day = 1; var week_no = this.by_data.BYWEEKNO[this.by_indices.BYWEEKNO]; this.last.day += 7 * week_no; if (end_of_data) { this.increment_year(1); } } else { // Jump to the next week this.increment_monthday(7 * this.rule.interval); } return end_of_data; }, /** * Normalize each by day rule for a given year/month. * Takes into account ordering and negative rules * * @private * @param {Number} year Current year. * @param {Number} month Current month. * @param {Array} rules Array of rules. * * @return {Array} sorted and normalized rules. * Negative rules will be expanded to their * correct positive values for easier processing. */ normalizeByMonthDayRules: function(year, month, rules) { var daysInMonth = ICAL.Time.daysInMonth(month, year); // XXX: This is probably bad for performance to allocate // a new array for each month we scan, if possible // we should try to optimize this... var newRules = []; var ruleIdx = 0; var len = rules.length; var rule; for (; ruleIdx < len; ruleIdx++) { rule = rules[ruleIdx]; // if this rule falls outside of given // month discard it. if (Math.abs(rule) > daysInMonth) { continue; } // negative case if (rule < 0) { // we add (not subtract its a negative number) // one from the rule because 1 === last day of month rule = daysInMonth + (rule + 1); } else if (rule === 0) { // skip zero its invalid. continue; } // only add unique items... if (newRules.indexOf(rule) === -1) { newRules.push(rule); } } // unique and sort return newRules.sort(function(a, b) { return a - b; }); }, /** * NOTES: * We are given a list of dates in the month (BYMONTHDAY) (23, etc..) * Also we are given a list of days (BYDAY) (MO, 2SU, etc..) when * both conditions match a given date (this.last.day) iteration stops. * * @private * @param {Boolean=} isInit When given true will not increment the * current day (this.last). */ _byDayAndMonthDay: function(isInit) { var byMonthDay; // setup in initMonth var byDay = this.by_data.BYDAY; var date; var dateIdx = 0; var dateLen; // setup in initMonth var dayLen = byDay.length; // we are not valid by default var dataIsValid = 0; var daysInMonth; var self = this; // we need a copy of this, because a DateTime gets normalized // automatically if the day is out of range. At some points we // set the last day to 0 to start counting. var lastDay = this.last.day; function initMonth() { daysInMonth = ICAL.Time.daysInMonth( self.last.month, self.last.year ); byMonthDay = self.normalizeByMonthDayRules( self.last.year, self.last.month, self.by_data.BYMONTHDAY ); dateLen = byMonthDay.length; // For the case of more than one occurrence in one month // we have to be sure to start searching after the last // found date or at the last BYMONTHDAY, unless we are // initializing the iterator because in this case we have // to consider the last found date too. while (byMonthDay[dateIdx] <= lastDay && !(isInit && byMonthDay[dateIdx] == lastDay) && dateIdx < dateLen - 1) { dateIdx++; } } function nextMonth() { // since the day is incremented at the start // of the loop below, we need to start at 0 lastDay = 0; self.increment_month(); dateIdx = 0; initMonth(); } initMonth(); // should come after initMonth if (isInit) { lastDay -= 1; } // Use a counter to avoid an infinite loop with malformed rules. // Stop checking after 4 years so we consider also a leap year. var monthsCounter = 48; while (!dataIsValid && monthsCounter) { monthsCounter--; // increment the current date. This is really // important otherwise we may fall into the infinite // loop trap. The initial date takes care of the case // where the current date is the date we are looking // for. date = lastDay + 1; if (date > daysInMonth) { nextMonth(); continue; } // find next date var next = byMonthDay[dateIdx++]; // this logic is dependant on the BYMONTHDAYS // being in order (which is done by #normalizeByMonthDayRules) if (next >= date) { // if the next month day is in the future jump to it. lastDay = next; } else { // in this case the 'next' monthday has past // we must move to the month. nextMonth(); continue; } // Now we can loop through the day rules to see // if one matches the current month date. for (var dayIdx = 0; dayIdx < dayLen; dayIdx++) { var parts = this.ruleDayOfWeek(byDay[dayIdx]); var pos = parts[0]; var dow = parts[1]; this.last.day = lastDay; if (this.last.isNthWeekDay(dow, pos)) { // when we find the valid one we can mark // the conditions as met and break the loop. // (Because we have this condition above // it will also break the parent loop). dataIsValid = 1; break; } } // Its completely possible that the combination // cannot be matched in the current month. // When we reach the end of possible combinations // in the current month we iterate to the next one. // since dateIdx is incremented right after getting // "next", we don't need dateLen -1 here. if (!dataIsValid && dateIdx === dateLen) { nextMonth(); continue; } } if (monthsCounter <= 0) { // Checked 4 years without finding a Byday that matches // a Bymonthday. Maybe the rule is not correct. throw new Error("Malformed values in BYDAY combined with BYMONTHDAY parts"); } return dataIsValid; }, next_month: function next_month() { var this_freq = (this.rule.freq == "MONTHLY"); var data_valid = 1; if (this.next_hour() == 0) { return data_valid; } if (this.has_by_data("BYDAY") && this.has_by_data("BYMONTHDAY")) { data_valid = this._byDayAndMonthDay(); } else if (this.has_by_data("BYDAY")) { var daysInMonth = ICAL.Time.daysInMonth(this.last.month, this.last.year); var setpos = 0; var setpos_total = 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)) { setpos_total++; if (day <= last_day) { setpos++; } } } this.last.day = last_day; } data_valid = 0; 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 - setpos_total - 1)) { data_valid = 1; break; } } } if (day > daysInMonth) { this.last.day = 1; this.increment_month(); if (this.is_day_in_byday(this.last)) { if (!this.has_by_data("BYSETPOS") || this.check_set_position(1)) { data_valid = 1; } } else { data_valid = 0; } } } else if (this.has_by_data("BYMONTHDAY")) { this.by_indices.BYMONTHDAY++; if (this.by_indices.BYMONTHDAY >= this.by_data.BYMONTHDAY.length) { this.by_indices.BYMONTHDAY = 0; this.increment_month(); } var daysInMonth = ICAL.Time.daysInMonth(this.last.month, this.last.year); var day = this.by_data.BYMONTHDAY[this.by_indices.BYMONTHDAY]; if (day < 0) { day = daysInMonth + day + 1; } if (day > daysInMonth) { this.last.day = 1; data_valid = this.is_day_in_byday(this.last); } else { this.last.day = day; } } else { this.increment_month(); var daysInMonth = ICAL.Time.daysInMonth(this.last.month, this.last.year); if (this.by_data.BYMONTHDAY[0] > daysInMonth) { data_valid = 0; } else { this.last.day = this.by_data.BYMONTHDAY[0]; } } return data_valid; }, next_weekday_by_week: function next_weekday_by_week() { var end_of_data = 0; if (this.next_hour() == 0) { return end_of_data; } if (!this.has_by_data("BYDAY")) { return 1; } for (;;) { var tt = new ICAL.Time(); this.by_indices.BYDAY++; if (this.by_indices.BYDAY == Object.keys(this.by_data.BYDAY).length) { this.by_indices.BYDAY = 0; end_of_data = 1; } var coded_day = this.by_data.BYDAY[this.by_indices.BYDAY]; var parts = this.ruleDayOfWeek(coded_day); var dow = parts[1]; dow -= this.rule.wkst; if (dow < 0) { dow += 7; } tt.year = this.last.year; tt.month = this.last.month; tt.day = this.last.day; var startOfWeek = tt.startDoyWeek(this.rule.wkst); if (dow + startOfWeek < 1) { // The selected date is in the previous year if (!end_of_data) { continue; } } var next = ICAL.Time.fromDayOfYear(startOfWeek + dow, this.last.year); /** * The normalization horrors below are due to * the fact that when the year/month/day changes * it can effect the other operations that come after. */ this.last.year = next.year; this.last.month = next.month; this.last.day = next.day; return end_of_data; } }, next_year: function next_year() { if (this.next_hour() == 0) { return 0; } if (++this.days_index == this.days.length) { this.days_index = 0; do { this.increment_year(this.rule.interval); this.expand_year_days(this.last.year); } while (this.days.length == 0); } this._nextByYearDay(); return 1; }, _nextByYearDay: function _nextByYearDay() { var doy = this.days[this.days_index]; var year = this.last.year; if (doy < 1) { // Time.fromDayOfYear(doy, year) indexes relative to the // start of the given year. That is different from the // semantics of BYYEARDAY where negative indexes are an // offset from the end of the given year. doy += 1; year += 1; } var next = ICAL.Time.fromDayOfYear(doy, year); this.last.day = next.day; this.last.month = next.month; }, ruleDayOfWeek: function ruleDayOfWeek(dow) { var matches = dow.match(/([+-]?[0-9])?(MO|TU|WE|TH|FR|SA|SU)/); if (matches) { var pos = parseInt(matches[1] || 0, 10); dow = ICAL.Recur.icalDayToNumericDay(matches[2]); return [pos, dow]; } else { return [0, 0]; } }, next_generic: function next_generic(aRuleType, aInterval, aDateAttr, aFollowingAttr, aPreviousIncr) { var has_by_rule = (aRuleType in this.by_data); var this_freq = (this.rule.freq == aInterval); var end_of_data = 0; if (aPreviousIncr && this[aPreviousIncr]() == 0) { return end_of_data; } if (has_by_rule) { this.by_indices[aRuleType]++; var idx = this.by_indices[aRuleType]; var dta = this.by_data[aRuleType]; if (this.by_indices[aRuleType] == dta.length) { this.by_indices[aRuleType] = 0; end_of_data = 1; } this.last[aDateAttr] = dta[this.by_indices[aRuleType]]; } else if (this_freq) { this["increment_" + aDateAttr](this.rule.interval); } if (has_by_rule && end_of_data && this_freq) { this["increment_" + aFollowingAttr](1); } return end_of_data; }, increment_monthday: function increment_monthday(inc) { for (var i = 0; i < inc; i++) { var daysInMonth = ICAL.Time.daysInMonth(this.last.month, this.last.year); this.last.day++; if (this.last.day > daysInMonth) { this.last.day -= daysInMonth; this.increment_month(); } } }, increment_month: function increment_month() { this.last.day = 1; if (this.has_by_data("BYMONTH")) { this.by_indices.BYMONTH++; if (this.by_indices.BYMONTH == this.by_data.BYMONTH.length) { this.by_indices.BYMONTH = 0; this.increment_year(1); } this.last.month = this.by_data.BYMONTH[this.by_indices.BYMONTH]; } else { if (this.rule.freq == "MONTHLY") { this.last.month += this.rule.interval; } else { this.last.month++; } this.last.month--; var years = ICAL.helpers.trunc(this.last.month / 12); this.last.month %= 12; this.last.month++; if (years != 0) { this.increment_year(years); } } }, increment_year: function increment_year(inc) { this.last.year += inc; }, increment_generic: function increment_generic(inc, aDateAttr, aFactor, aNextIncrement) { this.last[aDateAttr] += inc; var nextunit = ICAL.helpers.trunc(this.last[aDateAttr] / aFactor); this.last[aDateAttr] %= aFactor; if (nextunit != 0) { this["increment_" + aNextIncrement](nextunit); } }, has_by_data: function has_by_data(aRuleType) { return (aRuleType in this.rule.parts); }, expand_year_days: function expand_year_days(aYear) { var t = new ICAL.Time(); this.days = []; // We need our own copy with a few keys set var parts = {}; var rules = ["BYDAY", "BYWEEKNO", "BYMONTHDAY", "BYMONTH", "BYYEARDAY"]; for (var p in rules) { /* istanbul ignore else */ if (rules.hasOwnProperty(p)) { var part = rules[p]; if (part in this.rule.parts) { parts[part] = this.rule.parts[part]; } } } if ("BYMONTH" in parts && "BYWEEKNO" in parts) { var valid = 1; var validWeeks = {}; t.year = aYear; t.isDate = true; for (var monthIdx = 0; monthIdx < this.by_data.BYMONTH.length; monthIdx++) { var month = this.by_data.BYMONTH[monthIdx]; t.month = month; t.day = 1; var first_week = t.weekNumber(this.rule.wkst); t.day = ICAL.Time.daysInMonth(month, aYear); var last_week = t.weekNumber(this.rule.wkst); for (monthIdx = first_week; monthIdx < last_week; monthIdx++) { validWeeks[monthIdx] = 1; } } for (var weekIdx = 0; weekIdx < this.by_data.BYWEEKNO.length && valid; weekIdx++) { var weekno = this.by_data.BYWEEKNO[weekIdx]; if (weekno < 52) { valid &= validWeeks[weekIdx]; } else { valid = 0; } } if (valid) { delete parts.BYMONTH; } else { delete parts.BYWEEKNO; } } var partCount = Object.keys(parts).length; if (partCount == 0) { var t1 = this.dtstart.clone(); t1.year = this.last.year; this.days.push(t1.dayOfYear()); } else if (partCount == 1 && "BYMONTH" in parts) { for (var monthkey in this.by_data.BYMONTH) { /* istanbul ignore if */ if (!this.by_data.BYMONTH.hasOwnProperty(monthkey)) { continue; } 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) { /* istanbul ignore if */ if (!this.by_data.BYMONTHDAY.hasOwnProperty(monthdaykey)) { continue; } var t3 = this.dtstart.clone(); var day_ = this.by_data.BYMONTHDAY[monthdaykey]; if (day_ < 0) { var daysInMonth = ICAL.Time.daysInMonth(t3.month, aYear); day_ = day_ + daysInMonth + 1; } t3.day = day_; t3.year = aYear; t3.isDate = true; this.days.push(t3.dayOfYear()); } } else if (partCount == 2 && "BYMONTHDAY" in parts && "BYMONTH" in parts) { for (var monthkey in this.by_data.BYMONTH) { /* istanbul ignore if */ if (!this.by_data.BYMONTH.hasOwnProperty(monthkey)) { continue; } var month_ = this.by_data.BYMONTH[monthkey]; var daysInMonth = ICAL.Time.daysInMonth(month_, aYear); for (var monthdaykey in this.by_data.BYMONTHDAY) { /* istanbul ignore if */ if (!this.by_data.BYMONTHDAY.hasOwnProperty(monthdaykey)) { continue; } var day_ = this.by_data.BYMONTHDAY[monthdaykey]; if (day_ < 0) { day_ = day_ + daysInMonth + 1; } t.day = day_; t.month = month_; 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) { /* istanbul ignore if */ if (!this.by_data.BYMONTH.hasOwnProperty(monthkey)) { continue; } var month = this.by_data.BYMONTH[monthkey]; var daysInMonth = ICAL.Time.daysInMonth(month, aYear); t.year = aYear; t.month = this.by_data.BYMONTH[monthkey]; t.day = 1; t.isDate = true; var first_dow = t.dayOfWeek(); var doy_offset = t.dayOfYear() - 1; t.day = daysInMonth; var last_dow = t.dayOfWeek(); if (this.has_by_data("BYSETPOS")) { var set_pos_counter = 0; var by_month_day = []; for (var day = 1; day <= daysInMonth; day++) { t.day = day; if (this.is_day_in_byday(t)) { by_month_day.push(day); } } for (var spIndex = 0; spIndex < by_month_day.length; spIndex++) { if (this.check_set_position(spIndex + 1) || this.check_set_position(spIndex - by_month_day.length)) { this.days.push(doy_offset + by_month_day[spIndex]); } } } else { for (var daycodedkey in this.by_data.BYDAY) { /* istanbul ignore if */ if (!this.by_data.BYDAY.hasOwnProperty(daycodedkey)) { continue; } var coded_day = this.by_data.BYDAY[daycodedkey]; var bydayParts = this.ruleDayOfWeek(coded_day); var pos = bydayParts[0]; var dow = bydayParts[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); } } } } } // Return dates in order of occurrence (1,2,3,...) instead // of by groups of weekdays (1,8,15,...,2,9,16,...). this.days.sort(function(a, b) { return a - b; }); // Comparator function allows to sort numbers. } else if (partCount == 2 && "BYDAY" in parts && "BYMONTHDAY" in parts) { var expandedDays = this.expand_by_day(aYear); for (var daykey in expandedDays) { /* istanbul ignore if */ if (!expandedDays.hasOwnProperty(daykey)) { continue; } var day = expandedDays[daykey]; var tt = ICAL.Time.fromDayOfYear(day, aYear); if (this.by_data.BYMONTHDAY.indexOf(tt.day) >= 0) { this.days.push(day); } } } else if (partCount == 3 && "BYDAY" in parts && "BYMONTHDAY" in parts && "BYMONTH" in parts) { var expandedDays = this.expand_by_day(aYear); for (var daykey in expandedDays) { /* istanbul ignore if */ if (!expandedDays.hasOwnProperty(daykey)) { continue; } var day = expandedDays[daykey]; var tt = ICAL.Time.fromDayOfYear(day, aYear); if (this.by_data.BYMONTH.indexOf(tt.month) >= 0 && this.by_data.BYMONTHDAY.indexOf(tt.day) >= 0) { this.days.push(day); } } } else if (partCount == 2 && "BYDAY" in parts && "BYWEEKNO" in parts) { var expandedDays = this.expand_by_day(aYear); for (var daykey in expandedDays) { /* istanbul ignore if */ if (!expandedDays.hasOwnProperty(daykey)) { continue; } var day = expandedDays[daykey]; var tt = ICAL.Time.fromDayOfYear(day, aYear); var weekno = tt.weekNumber(this.rule.wkst); if (this.by_data.BYWEEKNO.indexOf(weekno)) { this.days.push(day); } } } else if (partCount == 3 && "BYDAY" in parts && "BYWEEKNO" in parts && "BYMONTHDAY" in parts) { // TODO unimplemted in libical } else if (partCount == 1 && "BYYEARDAY" in parts) { this.days = this.days.concat(this.by_data.BYYEARDAY); } else { this.days = []; } return 0; }, expand_by_day: function expand_by_day(aYear) { var days_list = []; var tmp = this.last.clone(); tmp.year = aYear; tmp.month = 1; tmp.day = 1; tmp.isDate = true; var start_dow = tmp.dayOfWeek(); tmp.month = 12; tmp.day = 31; tmp.isDate = true; var end_dow = tmp.dayOfWeek(); var end_year_day = tmp.dayOfYear(); for (var daykey in this.by_data.BYDAY) { /* istanbul ignore if */ if (!this.by_data.BYDAY.hasOwnProperty(daykey)) { continue; } 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) { /* istanbul ignore if */ if (!this.by_data.BYDAY.hasOwnProperty(daykey)) { continue; } 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. * * @private * @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) { /* istanbul ignore else */ if (ruleType.hasOwnProperty(bydatakey)) { if (ruleType[bydatakey] == v) { pass = true; break; } } } } else { // Not a contracting byrule or has no data, test passes pass = true; } return pass; }, check_contracting_rules: function check_contracting_rules() { var dow = this.last.dayOfWeek(); var weekNo = this.last.weekNumber(this.rule.wkst); var doy = this.last.dayOfYear(); return (this.check_contract_restriction("BYSECOND", this.last.second) && this.check_contract_restriction("BYMINUTE", this.last.minute) && this.check_contract_restriction("BYHOUR", this.last.hour) && this.check_contract_restriction("BYDAY", ICAL.Recur.numericDayToIcalDay(dow)) && this.check_contract_restriction("BYWEEKNO", weekNo) && this.check_contract_restriction("BYMONTHDAY", this.last.day) && this.check_contract_restriction("BYMONTH", this.last.month) && this.check_contract_restriction("BYYEARDAY", doy)); }, setup_defaults: function setup_defaults(aRuleType, req, deftime) { var indexMapValue = icalrecur_iterator._indexMap[aRuleType]; var ruleMapValue = icalrecur_iterator._expandMap[this.rule.freq][indexMapValue]; if (ruleMapValue != icalrecur_iterator.CONTRACT) { if (!(aRuleType in this.by_data)) { this.by_data[aRuleType] = [deftime]; } if (this.rule.freq != req) { return this.by_data[aRuleType][0]; } } return deftime; }, /** * Convert iterator into a serialize-able object. Will preserve current * iteration sequence to ensure the seamless continuation of the recurrence * rule. * @return {Object} */ toJSON: function() { var result = Object.create(null); result.initialized = this.initialized; result.rule = this.rule.toJSON(); result.dtstart = this.dtstart.toJSON(); result.by_data = this.by_data; result.days = this.days; result.last = this.last.toJSON(); result.by_indices = this.by_indices; result.occurrence_number = this.occurrence_number; return result; } }; icalrecur_iterator._indexMap = { "BYSECOND": 0, "BYMINUTE": 1, "BYHOUR": 2, "BYDAY": 3, "BYMONTHDAY": 4, "BYYEARDAY": 5, "BYWEEKNO": 6, "BYMONTH": 7, "BYSETPOS": 8 }; icalrecur_iterator._expandMap = { "SECONDLY": [1, 1, 1, 1, 1, 1, 1, 1], "MINUTELY": [2, 1, 1, 1, 1, 1, 1, 1], "HOURLY": [2, 2, 1, 1, 1, 1, 1, 1], "DAILY": [2, 2, 2, 1, 1, 1, 1, 1], "WEEKLY": [2, 2, 2, 2, 3, 3, 1, 1], "MONTHLY": [2, 2, 2, 2, 2, 3, 3, 1], "YEARLY": [2, 2, 2, 2, 2, 2, 2, 2] }; icalrecur_iterator.UNKNOWN = 0; icalrecur_iterator.CONTRACT = 1; icalrecur_iterator.EXPAND = 2; icalrecur_iterator.ILLEGAL = 3; return icalrecur_iterator; }()); /* 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-2015 */ /** * This symbol is further described later on * @ignore */ ICAL.RecurExpansion = (function() { function formatTime(item) { return ICAL.helpers.formatClassType(item, ICAL.Time); } function compareTime(a, b) { return a.compare(b); } function isRecurringComponent(comp) { return comp.hasProperty('rdate') || comp.hasProperty('rrule') || comp.hasProperty('recurrence-id'); } /** * @classdesc * 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. * * @example * // assuming event is a parsed ical component * var event; * * var expand = new ICAL.RecurExpansion({ * component: event, * start: event.getFirstPropertyValue('DTSTART') * }); * * // remember there are infinite rules * // so its a good idea to limit the scope * // of the iterations then resume later on. * * // next is always an ICAL.Time or null * var next; * * while (someCondition && (next = expand.next())) { * // do something with next * } * * // save instance for later * var json = JSON.stringify(expand); * * //... * * // NOTE: if the component's properties have * // changed you will need to rebuild the * // class and start over. This only works * // when the component's recurrence info is the same. * var expand = new ICAL.RecurExpansion(JSON.parse(json)); * * @description * The options object can be filled with the specified initial values. It can * also contain additional members, as a result of serializing a previous * expansion state, as shown in the example. * * @class * @alias ICAL.RecurExpansion * @param {Object} options * Recurrence expansion options * @param {ICAL.Time} options.dtstart * Start time of the event * @param {ICAL.Component=} options.component * Component for expansion, required if not resuming. */ function RecurExpansion(options) { this.ruleDates = []; this.exDates = []; this.fromData(options); } RecurExpansion.prototype = { /** * True when iteration is fully completed. * @type {Boolean} */ complete: false, /** * Array of rrule iterators. * * @type {ICAL.RecurIterator[]} * @private */ ruleIterators: null, /** * Array of rdate instances. * * @type {ICAL.Time[]} * @private */ ruleDates: null, /** * Array of exdate instances. * * @type {ICAL.Time[]} * @private */ exDates: null, /** * Current position in ruleDates array. * @type {Number} * @private */ ruleDateInc: 0, /** * Current position in exDates array * @type {Number} * @private */ exDateInc: 0, /** * Current negative date. * * @type {ICAL.Time} * @private */ exDate: null, /** * Current additional date. * * @type {ICAL.Time} * @private */ ruleDate: null, /** * Start date of recurring rules. * * @type {ICAL.Time} */ dtstart: null, /** * Last expanded time * * @type {ICAL.Time} */ last: null, /** * Initialize the recurrence expansion from the data object. The options * object may also contain additional members, see the * {@link ICAL.RecurExpansion constructor} for more details. * * @param {Object} options * Recurrence expansion options * @param {ICAL.Time} options.dtstart * Start time of the event * @param {ICAL.Component=} options.component * Component for expansion, required if not resuming. */ fromData: function(options) { var start = ICAL.helpers.formatClassType(options.dtstart, ICAL.Time); if (!start) { throw new Error('.dtstart (ICAL.Time) must be given'); } else { this.dtstart = start; } if (options.component) { this._init(options.component); } else { this.last = formatTime(options.last) || start.clone(); if (!options.ruleIterators) { throw new Error('.ruleIterators or .component must be given'); } this.ruleIterators = options.ruleIterators.map(function(item) { return ICAL.helpers.formatClassType(item, ICAL.RecurIterator); }); this.ruleDateInc = options.ruleDateInc; this.exDateInc = options.exDateInc; if (options.ruleDates) { this.ruleDates = options.ruleDates.map(formatTime); this.ruleDate = this.ruleDates[this.ruleDateInc]; } if (options.exDates) { this.exDates = options.exDates.map(formatTime); this.exDate = this.exDates[this.exDateInc]; } if (typeof(options.complete) !== 'undefined') { this.complete = options.complete; } } }, /** * Retrieve the next occurrence in the series. * @return {ICAL.Time} */ 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. This format can be passed * back into the expansion to resume iteration. * @return {Object} */ 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; }, /** * Extract all dates from the properties in the given component. The * properties will be filtered by the property name. * * @private * @param {ICAL.Component} component The component to search in * @param {String} propertyName The property name to search for * @return {ICAL.Time[]} The extracted dates. */ _extractDates: function(component, propertyName) { function handleProp(prop) { idx = ICAL.helpers.binsearchInsert( result, prop, compareTime ); // ordered insert result.splice(idx, 0, prop); } var result = []; var props = component.getAllProperties(propertyName); var len = props.length; var i = 0; var prop; var idx; for (; i < len; i++) { props[i].getValues().forEach(handleProp); } return result; }, /** * Initialize the recurrence expansion. * * @private * @param {ICAL.Component} component The component to initialize from. */ _init: function(component) { this.ruleIterators = []; this.last = this.dtstart.clone(); // to provide api consistency non-recurring // events can also use the iterator though it will // only return a single time. if (!isRecurringComponent(component)) { this.ruleDate = this.last.clone(); this.complete = true; return; } if (component.hasProperty('rdate')) { this.ruleDates = this._extractDates(component, 'rdate'); // special hack for cases where first rdate is prior // to the start date. We only check for the first rdate. // This is mostly for google's crazy recurring date logic // (contacts birthdays). if ((this.ruleDates[0]) && (this.ruleDates[0].compare(this.dtstart) < 0)) { this.ruleDateInc = 0; this.last = this.ruleDates[0].clone(); } else { this.ruleDateInc = ICAL.helpers.binsearchInsert( this.ruleDates, this.last, compareTime ); } this.ruleDate = this.ruleDates[this.ruleDateInc]; } if (component.hasProperty('rrule')) { var rules = component.getAllProperties('rrule'); var i = 0; var len = rules.length; var rule; var iter; for (; i < len; i++) { rule = rules[i].getFirstValue(); iter = rule.iterator(this.dtstart); this.ruleIterators.push(iter); // increment to the next occurrence so future // calls to next return times beyond the initial iteration. // XXX: I find this suspicious might be a bug? iter.next(); } } if (component.hasProperty('exdate')) { this.exDates = this._extractDates(component, 'exdate'); // if we have a .last day we increment the index to beyond it. this.exDateInc = ICAL.helpers.binsearchInsert( this.exDates, this.last, compareTime ); this.exDate = this.exDates[this.exDateInc]; } }, /** * Advance to the next exdate * @private */ _nextExDay: function() { this.exDate = this.exDates[++this.exDateInc]; }, /** * Advance to the next rule date * @private */ _nextRuleDay: function() { this.ruleDate = this.ruleDates[++this.ruleDateInc]; }, /** * Find and return the recurrence rule with the most recent event and * return it. * * @private * @return {?ICAL.RecurIterator} Found 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; }()); /* 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-2015 */ /** * This symbol is further described later on * @ignore */ ICAL.Event = (function() { /** * @classdesc * ICAL.js is organized into multiple layers. The bottom layer is a raw jCal * object, followed by the component/property layer. The highest level is the * event representation, which this class is part of. See the * {@tutorial layers} guide for more details. * * @class * @alias ICAL.Event * @param {ICAL.Component=} component The ICAL.Component to base this event on * @param {Object} options Options for this event * @param {Boolean} options.strictExceptions * When true, will verify exceptions are related by their UUID * @param {Array} options.exceptions * Exceptions to this event, either as components or events */ function Event(component, options) { if (!(component instanceof ICAL.Component)) { options = component; component = null; } if (component) { this.component = component; } else { this.component = new ICAL.Component('vevent'); } this._rangeExceptionCache = Object.create(null); this.exceptions = Object.create(null); this.rangeExceptions = []; if (options && options.strictExceptions) { this.strictExceptions = options.strictExceptions; } if (options && options.exceptions) { options.exceptions.forEach(this.relateException, this); } } Event.prototype = { THISANDFUTURE: 'THISANDFUTURE', /** * List of related event exceptions. * * @type {ICAL.Event[]} */ exceptions: null, /** * When true, will verify exceptions are related by their UUID. * * @type {Boolean} */ strictExceptions: false, /** * Relates a given event exception to this object. If the given component * does not share the UID of this event it cannot be related and will throw * an exception. * * If this component is an exception it cannot have other exceptions * related to it. * * @param {ICAL.Component|ICAL.Event} obj Component or event */ relateException: function(obj) { if (this.isRecurrenceException()) { throw new Error('cannot relate exception to exceptions'); } if (obj instanceof ICAL.Component) { obj = new ICAL.Event(obj); } if (this.strictExceptions && obj.uid !== this.uid) { throw new Error('attempted to relate unrelated exception'); } var id = obj.recurrenceId.toString(); // we don't sort or manage exceptions directly // here the recurrence expander handles that. this.exceptions[id] = obj; // index RANGE=THISANDFUTURE exceptions so we can // look them up later in getOccurrenceDetails. if (obj.modifiesFuture()) { var item = [ obj.recurrenceId.toUnixTime(), id ]; // we keep them sorted so we can find the nearest // value later on... var idx = ICAL.helpers.binsearchInsert( this.rangeExceptions, item, compareRangeException ); this.rangeExceptions.splice(idx, 0, item); } }, /** * Checks if this record is an exception and has the RANGE=THISANDFUTURE * value. * * @return {Boolean} True, when exception is within range */ modifiesFuture: function() { var range = this.component.getFirstPropertyValue('range'); return range === this.THISANDFUTURE; }, /** * Finds the range exception nearest to the given date. * * @param {ICAL.Time} time usually an occurrence time of an event * @return {?ICAL.Event} the related event/exception or null */ findRangeException: function(time) { if (!this.rangeExceptions.length) { return null; } var utc = time.toUnixTime(); var idx = ICAL.helpers.binsearchInsert( this.rangeExceptions, [utc], compareRangeException ); idx -= 1; // occurs before if (idx < 0) { return null; } var rangeItem = this.rangeExceptions[idx]; /* istanbul ignore next: sanity check only */ if (utc < rangeItem[0]) { return null; } return rangeItem[1]; }, /** * This object is returned by {@link ICAL.Event#getOccurrenceDetails getOccurrenceDetails} * * @typedef {Object} occurrenceDetails * @memberof ICAL.Event * @property {ICAL.Time} recurrenceId The passed in recurrence id * @property {ICAL.Event} item The occurrence * @property {ICAL.Time} startDate The start of the occurrence * @property {ICAL.Time} endDate The end of the occurrence */ /** * 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 {@link ICAL.Event#iterator iterator} method. * * @param {ICAL.Time} occurrence time occurrence * @return {ICAL.Event.occurrenceDetails} Information about the occurrence */ getOccurrenceDetails: function(occurrence) { var id = occurrence.toString(); var utcId = occurrence.convertToZone(ICAL.Timezone.utcTimezone).toString(); var item; var result = { //XXX: Clone? recurrenceId: occurrence }; if (id in this.exceptions) { item = result.item = this.exceptions[id]; result.startDate = item.startDate; result.endDate = item.endDate; result.item = item; } else if (utcId in this.exceptions) { item = this.exceptions[utcId]; result.startDate = item.startDate; result.endDate = item.endDate; result.item = item; } else { // range exceptions (RANGE=THISANDFUTURE) have a // lower priority then direct exceptions but // must be accounted for first. Their item is // always the first exception with the range prop. var rangeExceptionId = this.findRangeException( occurrence ); var end; if (rangeExceptionId) { var exception = this.exceptions[rangeExceptionId]; // range exception must modify standard time // by the difference (if any) in start/end times. result.item = exception; var startDiff = this._rangeExceptionCache[rangeExceptionId]; if (!startDiff) { var original = exception.recurrenceId.clone(); var newStart = exception.startDate.clone(); // zones must be same otherwise subtract may be incorrect. original.zone = newStart.zone; startDiff = newStart.subtractDate(original); this._rangeExceptionCache[rangeExceptionId] = startDiff; } var start = occurrence.clone(); start.zone = exception.startDate.zone; start.addDuration(startDiff); end = start.clone(); end.addDuration(exception.duration); result.startDate = start; result.endDate = end; } else { // no range exception standard expansion 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). * * @param {ICAL.Time} startTime Starting point for expansion * @return {ICAL.RecurExpansion} Expansion object */ iterator: function(startTime) { return new ICAL.RecurExpansion({ component: this.component, dtstart: startTime || this.startDate }); }, /** * Checks if the event is recurring * * @return {Boolean} True, if event is recurring */ isRecurring: function() { var comp = this.component; return comp.hasProperty('rrule') || comp.hasProperty('rdate'); }, /** * Checks if the event describes a recurrence exception. See * {@tutorial terminology} for details. * * @return {Boolean} True, if the even describes a recurrence exception */ 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; }, /** * The uid of this event * @type {String} */ get uid() { return this._firstProp('uid'); }, set uid(value) { this._setProp('uid', value); }, /** * The start date * @type {ICAL.Time} */ get startDate() { return this._firstProp('dtstart'); }, set startDate(value) { this._setTime('dtstart', value); }, /** * The end date. This can be the result directly from the property, or the * end date calculated from start date and duration. * @type {ICAL.Time} */ get endDate() { var endDate = this._firstProp('dtend'); if (!endDate) { var duration = this._firstProp('duration'); endDate = this.startDate.clone(); if (duration) { endDate.addDuration(duration); } else if (endDate.isDate) { endDate.day += 1; } } return endDate; }, set endDate(value) { this._setTime('dtend', value); }, /** * The duration. This can be the result directly from the property, or the * duration calculated from start date and end date. * @type {ICAL.Duration} * @readonly */ get duration() { var duration = this._firstProp('duration'); if (!duration) { return this.endDate.subtractDate(this.startDate); } return duration; }, /** * The location of the event. * @type {String} */ get location() { return this._firstProp('location'); }, set location(value) { return this._setProp('location', value); }, /** * The attendees in the event * @type {ICAL.Property[]} * @readonly */ get attendees() { //XXX: This is way lame we should have a better // data structure for this later. return this.component.getAllProperties('attendee'); }, /** * The event summary * @type {String} */ get summary() { return this._firstProp('summary'); }, set summary(value) { this._setProp('summary', value); }, /** * The event description. * @type {String} */ get description() { return this._firstProp('description'); }, set description(value) { this._setProp('description', value); }, /** * The organizer value as an uri. In most cases this is a mailto: uri, but * it can also be something else, like urn:uuid:... * @type {String} */ get organizer() { return this._firstProp('organizer'); }, set organizer(value) { this._setProp('organizer', value); }, /** * The sequence value for this event. Used for scheduling * see {@tutorial terminology}. * @type {Number} */ get sequence() { return this._firstProp('sequence'); }, set sequence(value) { this._setProp('sequence', value); }, /** * The recurrence id for this event. See {@tutorial terminology} for details. * @type {ICAL.Time} */ get recurrenceId() { return this._firstProp('recurrence-id'); }, set recurrenceId(value) { this._setProp('recurrence-id', value); }, /** * Set/update a time property's value. * This will also update the TZID of the property. * * TODO: this method handles the case where we are switching * from a known timezone to an implied timezone (one without TZID). * This does _not_ handle the case of moving between a known * (by TimezoneService) timezone to an unknown timezone... * * We will not add/remove/update the VTIMEZONE subcomponents * leading to invalid ICAL data... * @private * @param {String} propName The property name * @param {ICAL.Time} time The time to set */ _setTime: function(propName, time) { var prop = this.component.getFirstProperty(propName); if (!prop) { prop = new ICAL.Property(propName); this.component.addProperty(prop); } // utc and local don't get a tzid if ( time.zone === ICAL.Timezone.localTimezone || time.zone === ICAL.Timezone.utcTimezone ) { // remove the tzid prop.removeParameter('tzid'); } else { prop.setParameter('tzid', time.zone.tzid); } prop.setValue(time); }, _setProp: function(name, value) { this.component.updatePropertyWithValue(name, value); }, _firstProp: function(name) { return this.component.getFirstPropertyValue(name); }, /** * The string representation of this event. * @return {String} */ toString: function() { return this.component.toString(); } }; function compareRangeException(a, b) { if (a[0] > b[0]) return 1; if (b[0] > a[0]) return -1; return 0; } return Event; }()); /* 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-2015 */ /** * This symbol is further described later on * @ignore */ ICAL.ComponentParser = (function() { /** * @classdesc * The ComponentParser is used to process a String or jCal Object, * firing callbacks for various found components, as well as completion. * * @example * var options = { * // when false no events will be emitted for type * parseEvent: true, * parseTimezone: true * }; * * var parser = new ICAL.ComponentParser(options); * * parser.onevent(eventComponent) { * //... * } * * // ontimezone, etc... * * parser.oncomplete = function() { * * }; * * parser.process(stringOrComponent); * * @class * @alias ICAL.ComponentParser * @param {Object=} options Component parser options * @param {Boolean} options.parseEvent Whether events should be parsed * @param {Boolean} options.parseTimezeone Whether timezones should be parsed */ function ComponentParser(options) { if (typeof(options) === 'undefined') { options = {}; } var key; for (key in options) { /* istanbul ignore else */ 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 * @callback */ oncomplete: /* istanbul ignore next */ function() {}, /** * Fired if an error occurs during parsing. * * @callback * @param {Error} err details of error */ onerror: /* istanbul ignore next */ function(err) {}, /** * Fired when a top level component (VTIMEZONE) is found * * @callback * @param {ICAL.Timezone} component Timezone object */ ontimezone: /* istanbul ignore next */ function(component) {}, /** * Fired when a top level component (VEVENT) is found. * * @callback * @param {ICAL.Event} component Top level component */ onevent: /* istanbul ignore next */ 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 {ICAL.Component|String|Object} ical The component to process, * either in its final form, as a jCal Object, or string representation */ 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); } if (!(ical instanceof ICAL.Component)) { ical = new ICAL.Component(ical); } var components = ical.getAllSubcomponents(); var i = 0; var len = components.length; var component; for (; i < len; i++) { component = components[i]; switch (component.name) { case 'vtimezone': if (this.parseTimezone) { var tzid = component.getFirstPropertyValue('tzid'); if (tzid) { this.ontimezone(new ICAL.Timezone({ tzid: tzid, component: component })); } } break; case 'vevent': if (this.parseEvent) { this.onevent(new ICAL.Event(component)); } break; default: continue; } } //XXX: ideally we should do a "nextTick" here // so in all cases this is actually async. this.oncomplete(); } }; return ComponentParser; }());