1/* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5/** 6 * This is ical.js from <https://github.com/mozilla-comm/ical.js>. 7 * 8 * If you would like to change anything in ical.js, it is required to do so 9 * upstream first. 10 * 11 * Current ical.js git revision: 7c99a434b38ad5d670a0e778ef055a2e17ccb3db (v1.4.0) 12 */ 13 14var EXPORTED_SYMBOLS = ["ICAL", "unwrap", "unwrapSetter", "unwrapSingle", "wrapGetter"]; 15 16function wrapGetter(type, val) { 17 return val ? new type(val) : null; 18} 19 20function unwrap(type, innerFunc) { 21 return function(val) { return unwrapSetter.call(this, type, val, innerFunc); }; 22} 23 24function unwrapSetter(type, val, innerFunc, thisObj) { 25 return innerFunc.call(thisObj || this, unwrapSingle(type, val)); 26} 27 28function unwrapSingle(type, val) { 29 if (!val || !val.wrappedJSObject) { 30 return null; 31 } else if (val.wrappedJSObject.innerObject instanceof type) { 32 return val.wrappedJSObject.innerObject; 33 } else { 34 var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm"); 35 Cu.reportError("Unknown " + (type.icalclass || type) + " passed at " + cal.STACK(10)); 36 return null; 37 } 38} 39 40// -- start ical.js -- 41 42/* This Source Code Form is subject to the terms of the Mozilla Public 43 * License, v. 2.0. If a copy of the MPL was not distributed with this 44 * file, You can obtain one at http://mozilla.org/MPL/2.0/. 45 * Portions Copyright (C) Philipp Kewisch, 2011-2015 */ 46 47var ICAL; 48 49/* istanbul ignore next */ 50/* jshint ignore:start */ 51if (typeof module === 'object') { 52 // CommonJS, where exports may be different each time. 53 ICAL = module.exports; 54} else if (typeof ICAL !== 'object') {/* istanbul ignore next */ 55 /** @ignore */ 56 ICAL = {}; 57} 58/* jshint ignore:end */ 59 60 61/** 62 * The number of characters before iCalendar line folding should occur 63 * @type {Number} 64 * @default 75 65 */ 66ICAL.foldLength = 75; 67 68 69/** 70 * The character(s) to be used for a newline. The default value is provided by 71 * rfc5545. 72 * @type {String} 73 * @default "\r\n" 74 */ 75ICAL.newLineChar = '\r\n'; 76 77 78/** 79 * Helper functions used in various places within ical.js 80 * @namespace 81 */ 82ICAL.helpers = { 83 /** 84 * Compiles a list of all referenced TZIDs in all subcomponents and 85 * removes any extra VTIMEZONE subcomponents. In addition, if any TZIDs 86 * are referenced by a component, but a VTIMEZONE does not exist, 87 * an attempt will be made to generate a VTIMEZONE using ICAL.TimezoneService. 88 * 89 * @param {ICAL.Component} vcal The top-level VCALENDAR component. 90 * @return {ICAL.Component} The ICAL.Component that was passed in. 91 */ 92 updateTimezones: function(vcal) { 93 var allsubs, properties, vtimezones, reqTzid, i, tzid; 94 95 if (!vcal || vcal.name !== "vcalendar") { 96 //not a top-level vcalendar component 97 return vcal; 98 } 99 100 //Store vtimezone subcomponents in an object reference by tzid. 101 //Store properties from everything else in another array 102 allsubs = vcal.getAllSubcomponents(); 103 properties = []; 104 vtimezones = {}; 105 for (i = 0; i < allsubs.length; i++) { 106 if (allsubs[i].name === "vtimezone") { 107 tzid = allsubs[i].getFirstProperty("tzid").getFirstValue(); 108 vtimezones[tzid] = allsubs[i]; 109 } else { 110 properties = properties.concat(allsubs[i].getAllProperties()); 111 } 112 } 113 114 //create an object with one entry for each required tz 115 reqTzid = {}; 116 for (i = 0; i < properties.length; i++) { 117 if ((tzid = properties[i].getParameter("tzid"))) { 118 reqTzid[tzid] = true; 119 } 120 } 121 122 //delete any vtimezones that are not on the reqTzid list. 123 for (i in vtimezones) { 124 if (vtimezones.hasOwnProperty(i) && !reqTzid[i]) { 125 vcal.removeSubcomponent(vtimezones[i]); 126 } 127 } 128 129 //create any missing, but registered timezones 130 for (i in reqTzid) { 131 if ( 132 reqTzid.hasOwnProperty(i) && 133 !vtimezones[i] && 134 ICAL.TimezoneService.has(i) 135 ) { 136 vcal.addSubcomponent(ICAL.TimezoneService.get(i).component); 137 } 138 } 139 140 return vcal; 141 }, 142 143 /** 144 * Checks if the given type is of the number type and also NaN. 145 * 146 * @param {Number} number The number to check 147 * @return {Boolean} True, if the number is strictly NaN 148 */ 149 isStrictlyNaN: function(number) { 150 return typeof(number) === 'number' && isNaN(number); 151 }, 152 153 /** 154 * Parses a string value that is expected to be an integer, when the valid is 155 * not an integer throws a decoration error. 156 * 157 * @param {String} string Raw string input 158 * @return {Number} Parsed integer 159 */ 160 strictParseInt: function(string) { 161 var result = parseInt(string, 10); 162 163 if (ICAL.helpers.isStrictlyNaN(result)) { 164 throw new Error( 165 'Could not extract integer from "' + string + '"' 166 ); 167 } 168 169 return result; 170 }, 171 172 /** 173 * Creates or returns a class instance of a given type with the initialization 174 * data if the data is not already an instance of the given type. 175 * 176 * @example 177 * var time = new ICAL.Time(...); 178 * var result = ICAL.helpers.formatClassType(time, ICAL.Time); 179 * 180 * (result instanceof ICAL.Time) 181 * // => true 182 * 183 * result = ICAL.helpers.formatClassType({}, ICAL.Time); 184 * (result isntanceof ICAL.Time) 185 * // => true 186 * 187 * 188 * @param {Object} data object initialization data 189 * @param {Object} type object type (like ICAL.Time) 190 * @return {?} An instance of the found type. 191 */ 192 formatClassType: function formatClassType(data, type) { 193 if (typeof(data) === 'undefined') { 194 return undefined; 195 } 196 197 if (data instanceof type) { 198 return data; 199 } 200 return new type(data); 201 }, 202 203 /** 204 * Identical to indexOf but will only match values when they are not preceded 205 * by a backslash character. 206 * 207 * @param {String} buffer String to search 208 * @param {String} search Value to look for 209 * @param {Number} pos Start position 210 * @return {Number} The position, or -1 if not found 211 */ 212 unescapedIndexOf: function(buffer, search, pos) { 213 while ((pos = buffer.indexOf(search, pos)) !== -1) { 214 if (pos > 0 && buffer[pos - 1] === '\\') { 215 pos += 1; 216 } else { 217 return pos; 218 } 219 } 220 return -1; 221 }, 222 223 /** 224 * Find the index for insertion using binary search. 225 * 226 * @param {Array} list The list to search 227 * @param {?} seekVal The value to insert 228 * @param {function(?,?)} cmpfunc The comparison func, that can 229 * compare two seekVals 230 * @return {Number} The insert position 231 */ 232 binsearchInsert: function(list, seekVal, cmpfunc) { 233 if (!list.length) 234 return 0; 235 236 var low = 0, high = list.length - 1, 237 mid, cmpval; 238 239 while (low <= high) { 240 mid = low + Math.floor((high - low) / 2); 241 cmpval = cmpfunc(seekVal, list[mid]); 242 243 if (cmpval < 0) 244 high = mid - 1; 245 else if (cmpval > 0) 246 low = mid + 1; 247 else 248 break; 249 } 250 251 if (cmpval < 0) 252 return mid; // insertion is displacing, so use mid outright. 253 else if (cmpval > 0) 254 return mid + 1; 255 else 256 return mid; 257 }, 258 259 /** 260 * Convenience function for debug output 261 * @private 262 */ 263 dumpn: /* istanbul ignore next */ function() { 264 if (!ICAL.debug) { 265 return; 266 } 267 268 if (typeof (console) !== 'undefined' && 'log' in console) { 269 ICAL.helpers.dumpn = function consoleDumpn(input) { 270 console.log(input); 271 }; 272 } else { 273 ICAL.helpers.dumpn = function geckoDumpn(input) { 274 dump(input + '\n'); 275 }; 276 } 277 278 ICAL.helpers.dumpn(arguments[0]); 279 }, 280 281 /** 282 * Clone the passed object or primitive. By default a shallow clone will be 283 * executed. 284 * 285 * @param {*} aSrc The thing to clone 286 * @param {Boolean=} aDeep If true, a deep clone will be performed 287 * @return {*} The copy of the thing 288 */ 289 clone: function(aSrc, aDeep) { 290 if (!aSrc || typeof aSrc != "object") { 291 return aSrc; 292 } else if (aSrc instanceof Date) { 293 return new Date(aSrc.getTime()); 294 } else if ("clone" in aSrc) { 295 return aSrc.clone(); 296 } else if (Array.isArray(aSrc)) { 297 var arr = []; 298 for (var i = 0; i < aSrc.length; i++) { 299 arr.push(aDeep ? ICAL.helpers.clone(aSrc[i], true) : aSrc[i]); 300 } 301 return arr; 302 } else { 303 var obj = {}; 304 for (var name in aSrc) { 305 // uses prototype method to allow use of Object.create(null); 306 /* istanbul ignore else */ 307 if (Object.prototype.hasOwnProperty.call(aSrc, name)) { 308 if (aDeep) { 309 obj[name] = ICAL.helpers.clone(aSrc[name], true); 310 } else { 311 obj[name] = aSrc[name]; 312 } 313 } 314 } 315 return obj; 316 } 317 }, 318 319 /** 320 * Performs iCalendar line folding. A line ending character is inserted and 321 * the next line begins with a whitespace. 322 * 323 * @example 324 * SUMMARY:This line will be fold 325 * ed right in the middle of a word. 326 * 327 * @param {String} aLine The line to fold 328 * @return {String} The folded line 329 */ 330 foldline: function foldline(aLine) { 331 var result = ""; 332 var line = aLine || ""; 333 334 while (line.length) { 335 result += ICAL.newLineChar + " " + line.substr(0, ICAL.foldLength); 336 line = line.substr(ICAL.foldLength); 337 } 338 return result.substr(ICAL.newLineChar.length + 1); 339 }, 340 341 /** 342 * Pads the given string or number with zeros so it will have at least two 343 * characters. 344 * 345 * @param {String|Number} data The string or number to pad 346 * @return {String} The number padded as a string 347 */ 348 pad2: function pad(data) { 349 if (typeof(data) !== 'string') { 350 // handle fractions. 351 if (typeof(data) === 'number') { 352 data = parseInt(data); 353 } 354 data = String(data); 355 } 356 357 var len = data.length; 358 359 switch (len) { 360 case 0: 361 return '00'; 362 case 1: 363 return '0' + data; 364 default: 365 return data; 366 } 367 }, 368 369 /** 370 * Truncates the given number, correctly handling negative numbers. 371 * 372 * @param {Number} number The number to truncate 373 * @return {Number} The truncated number 374 */ 375 trunc: function trunc(number) { 376 return (number < 0 ? Math.ceil(number) : Math.floor(number)); 377 }, 378 379 /** 380 * Poor-man's cross-browser inheritance for JavaScript. Doesn't support all 381 * the features, but enough for our usage. 382 * 383 * @param {Function} base The base class constructor function. 384 * @param {Function} child The child class constructor function. 385 * @param {Object} extra Extends the prototype with extra properties 386 * and methods 387 */ 388 inherits: function(base, child, extra) { 389 function F() {} 390 F.prototype = base.prototype; 391 child.prototype = new F(); 392 393 if (extra) { 394 ICAL.helpers.extend(extra, child.prototype); 395 } 396 }, 397 398 /** 399 * Poor-man's cross-browser object extension. Doesn't support all the 400 * features, but enough for our usage. Note that the target's properties are 401 * not overwritten with the source properties. 402 * 403 * @example 404 * var child = ICAL.helpers.extend(parent, { 405 * "bar": 123 406 * }); 407 * 408 * @param {Object} source The object to extend 409 * @param {Object} target The object to extend with 410 * @return {Object} Returns the target. 411 */ 412 extend: function(source, target) { 413 for (var key in source) { 414 var descr = Object.getOwnPropertyDescriptor(source, key); 415 if (descr && !Object.getOwnPropertyDescriptor(target, key)) { 416 Object.defineProperty(target, key, descr); 417 } 418 } 419 return target; 420 } 421}; 422/* This Source Code Form is subject to the terms of the Mozilla Public 423 * License, v. 2.0. If a copy of the MPL was not distributed with this 424 * file, You can obtain one at http://mozilla.org/MPL/2.0/. 425 * Portions Copyright (C) Philipp Kewisch, 2011-2015 */ 426 427/** @namespace ICAL */ 428 429 430/** 431 * This symbol is further described later on 432 * @ignore 433 */ 434ICAL.design = (function() { 435 'use strict'; 436 437 var FROM_ICAL_NEWLINE = /\\\\|\\;|\\,|\\[Nn]/g; 438 var TO_ICAL_NEWLINE = /\\|;|,|\n/g; 439 var FROM_VCARD_NEWLINE = /\\\\|\\,|\\[Nn]/g; 440 var TO_VCARD_NEWLINE = /\\|,|\n/g; 441 442 function createTextType(fromNewline, toNewline) { 443 var result = { 444 matches: /.*/, 445 446 fromICAL: function(aValue, structuredEscape) { 447 return replaceNewline(aValue, fromNewline, structuredEscape); 448 }, 449 450 toICAL: function(aValue, structuredEscape) { 451 var regEx = toNewline; 452 if (structuredEscape) 453 regEx = new RegExp(regEx.source + '|' + structuredEscape); 454 return aValue.replace(regEx, function(str) { 455 switch (str) { 456 case "\\": 457 return "\\\\"; 458 case ";": 459 return "\\;"; 460 case ",": 461 return "\\,"; 462 case "\n": 463 return "\\n"; 464 /* istanbul ignore next */ 465 default: 466 return str; 467 } 468 }); 469 } 470 }; 471 return result; 472 } 473 474 // default types used multiple times 475 var DEFAULT_TYPE_TEXT = { defaultType: "text" }; 476 var DEFAULT_TYPE_TEXT_MULTI = { defaultType: "text", multiValue: "," }; 477 var DEFAULT_TYPE_TEXT_STRUCTURED = { defaultType: "text", structuredValue: ";" }; 478 var DEFAULT_TYPE_INTEGER = { defaultType: "integer" }; 479 var DEFAULT_TYPE_DATETIME_DATE = { defaultType: "date-time", allowedTypes: ["date-time", "date"] }; 480 var DEFAULT_TYPE_DATETIME = { defaultType: "date-time" }; 481 var DEFAULT_TYPE_URI = { defaultType: "uri" }; 482 var DEFAULT_TYPE_UTCOFFSET = { defaultType: "utc-offset" }; 483 var DEFAULT_TYPE_RECUR = { defaultType: "recur" }; 484 var DEFAULT_TYPE_DATE_ANDOR_TIME = { defaultType: "date-and-or-time", allowedTypes: ["date-time", "date", "text"] }; 485 486 function replaceNewlineReplace(string) { 487 switch (string) { 488 case "\\\\": 489 return "\\"; 490 case "\\;": 491 return ";"; 492 case "\\,": 493 return ","; 494 case "\\n": 495 case "\\N": 496 return "\n"; 497 /* istanbul ignore next */ 498 default: 499 return string; 500 } 501 } 502 503 function replaceNewline(value, newline, structuredEscape) { 504 // avoid regex when possible. 505 if (value.indexOf('\\') === -1) { 506 return value; 507 } 508 if (structuredEscape) 509 newline = new RegExp(newline.source + '|\\\\' + structuredEscape); 510 return value.replace(newline, replaceNewlineReplace); 511 } 512 513 var commonProperties = { 514 "categories": DEFAULT_TYPE_TEXT_MULTI, 515 "url": DEFAULT_TYPE_URI, 516 "version": DEFAULT_TYPE_TEXT, 517 "uid": DEFAULT_TYPE_TEXT 518 }; 519 520 var commonValues = { 521 "boolean": { 522 values: ["TRUE", "FALSE"], 523 524 fromICAL: function(aValue) { 525 switch (aValue) { 526 case 'TRUE': 527 return true; 528 case 'FALSE': 529 return false; 530 default: 531 //TODO: parser warning 532 return false; 533 } 534 }, 535 536 toICAL: function(aValue) { 537 if (aValue) { 538 return 'TRUE'; 539 } 540 return 'FALSE'; 541 } 542 543 }, 544 float: { 545 matches: /^[+-]?\d+\.\d+$/, 546 547 fromICAL: function(aValue) { 548 var parsed = parseFloat(aValue); 549 if (ICAL.helpers.isStrictlyNaN(parsed)) { 550 // TODO: parser warning 551 return 0.0; 552 } 553 return parsed; 554 }, 555 556 toICAL: function(aValue) { 557 return String(aValue); 558 } 559 }, 560 integer: { 561 fromICAL: function(aValue) { 562 var parsed = parseInt(aValue); 563 if (ICAL.helpers.isStrictlyNaN(parsed)) { 564 return 0; 565 } 566 return parsed; 567 }, 568 569 toICAL: function(aValue) { 570 return String(aValue); 571 } 572 }, 573 "utc-offset": { 574 toICAL: function(aValue) { 575 if (aValue.length < 7) { 576 // no seconds 577 // -0500 578 return aValue.substr(0, 3) + 579 aValue.substr(4, 2); 580 } else { 581 // seconds 582 // -050000 583 return aValue.substr(0, 3) + 584 aValue.substr(4, 2) + 585 aValue.substr(7, 2); 586 } 587 }, 588 589 fromICAL: function(aValue) { 590 if (aValue.length < 6) { 591 // no seconds 592 // -05:00 593 return aValue.substr(0, 3) + ':' + 594 aValue.substr(3, 2); 595 } else { 596 // seconds 597 // -05:00:00 598 return aValue.substr(0, 3) + ':' + 599 aValue.substr(3, 2) + ':' + 600 aValue.substr(5, 2); 601 } 602 }, 603 604 decorate: function(aValue) { 605 return ICAL.UtcOffset.fromString(aValue); 606 }, 607 608 undecorate: function(aValue) { 609 return aValue.toString(); 610 } 611 } 612 }; 613 614 var icalParams = { 615 // Although the syntax is DQUOTE uri DQUOTE, I don't think we should 616 // enfoce anything aside from it being a valid content line. 617 // 618 // At least some params require - if multi values are used - DQUOTEs 619 // for each of its values - e.g. delegated-from="uri1","uri2" 620 // To indicate this, I introduced the new k/v pair 621 // multiValueSeparateDQuote: true 622 // 623 // "ALTREP": { ... }, 624 625 // CN just wants a param-value 626 // "CN": { ... } 627 628 "cutype": { 629 values: ["INDIVIDUAL", "GROUP", "RESOURCE", "ROOM", "UNKNOWN"], 630 allowXName: true, 631 allowIanaToken: true 632 }, 633 634 "delegated-from": { 635 valueType: "cal-address", 636 multiValue: ",", 637 multiValueSeparateDQuote: true 638 }, 639 "delegated-to": { 640 valueType: "cal-address", 641 multiValue: ",", 642 multiValueSeparateDQuote: true 643 }, 644 // "DIR": { ... }, // See ALTREP 645 "encoding": { 646 values: ["8BIT", "BASE64"] 647 }, 648 // "FMTTYPE": { ... }, // See ALTREP 649 "fbtype": { 650 values: ["FREE", "BUSY", "BUSY-UNAVAILABLE", "BUSY-TENTATIVE"], 651 allowXName: true, 652 allowIanaToken: true 653 }, 654 // "LANGUAGE": { ... }, // See ALTREP 655 "member": { 656 valueType: "cal-address", 657 multiValue: ",", 658 multiValueSeparateDQuote: true 659 }, 660 "partstat": { 661 // TODO These values are actually different per-component 662 values: ["NEEDS-ACTION", "ACCEPTED", "DECLINED", "TENTATIVE", 663 "DELEGATED", "COMPLETED", "IN-PROCESS"], 664 allowXName: true, 665 allowIanaToken: true 666 }, 667 "range": { 668 values: ["THISLANDFUTURE"] 669 }, 670 "related": { 671 values: ["START", "END"] 672 }, 673 "reltype": { 674 values: ["PARENT", "CHILD", "SIBLING"], 675 allowXName: true, 676 allowIanaToken: true 677 }, 678 "role": { 679 values: ["REQ-PARTICIPANT", "CHAIR", 680 "OPT-PARTICIPANT", "NON-PARTICIPANT"], 681 allowXName: true, 682 allowIanaToken: true 683 }, 684 "rsvp": { 685 values: ["TRUE", "FALSE"] 686 }, 687 "sent-by": { 688 valueType: "cal-address" 689 }, 690 "tzid": { 691 matches: /^\// 692 }, 693 "value": { 694 // since the value here is a 'type' lowercase is used. 695 values: ["binary", "boolean", "cal-address", "date", "date-time", 696 "duration", "float", "integer", "period", "recur", "text", 697 "time", "uri", "utc-offset"], 698 allowXName: true, 699 allowIanaToken: true 700 } 701 }; 702 703 // When adding a value here, be sure to add it to the parameter types! 704 var icalValues = ICAL.helpers.extend(commonValues, { 705 text: createTextType(FROM_ICAL_NEWLINE, TO_ICAL_NEWLINE), 706 707 uri: { 708 // TODO 709 /* ... */ 710 }, 711 712 "binary": { 713 decorate: function(aString) { 714 return ICAL.Binary.fromString(aString); 715 }, 716 717 undecorate: function(aBinary) { 718 return aBinary.toString(); 719 } 720 }, 721 "cal-address": { 722 // needs to be an uri 723 }, 724 "date": { 725 decorate: function(aValue, aProp) { 726 if (design.strict) { 727 return ICAL.Time.fromDateString(aValue, aProp); 728 } else { 729 return ICAL.Time.fromString(aValue, aProp); 730 } 731 }, 732 733 /** 734 * undecorates a time object. 735 */ 736 undecorate: function(aValue) { 737 return aValue.toString(); 738 }, 739 740 fromICAL: function(aValue) { 741 // from: 20120901 742 // to: 2012-09-01 743 if (!design.strict && aValue.length >= 15) { 744 // This is probably a date-time, e.g. 20120901T130000Z 745 return icalValues["date-time"].fromICAL(aValue); 746 } else { 747 return aValue.substr(0, 4) + '-' + 748 aValue.substr(4, 2) + '-' + 749 aValue.substr(6, 2); 750 } 751 }, 752 753 toICAL: function(aValue) { 754 // from: 2012-09-01 755 // to: 20120901 756 var len = aValue.length; 757 758 if (len == 10) { 759 return aValue.substr(0, 4) + 760 aValue.substr(5, 2) + 761 aValue.substr(8, 2); 762 } else if (len >= 19) { 763 return icalValues["date-time"].toICAL(aValue); 764 } else { 765 //TODO: serialize warning? 766 return aValue; 767 } 768 769 } 770 }, 771 "date-time": { 772 fromICAL: function(aValue) { 773 // from: 20120901T130000 774 // to: 2012-09-01T13:00:00 775 if (!design.strict && aValue.length == 8) { 776 // This is probably a date, e.g. 20120901 777 return icalValues.date.fromICAL(aValue); 778 } else { 779 var result = aValue.substr(0, 4) + '-' + 780 aValue.substr(4, 2) + '-' + 781 aValue.substr(6, 2) + 'T' + 782 aValue.substr(9, 2) + ':' + 783 aValue.substr(11, 2) + ':' + 784 aValue.substr(13, 2); 785 786 if (aValue[15] && aValue[15] === 'Z') { 787 result += 'Z'; 788 } 789 790 return result; 791 } 792 }, 793 794 toICAL: function(aValue) { 795 // from: 2012-09-01T13:00:00 796 // to: 20120901T130000 797 var len = aValue.length; 798 799 if (len == 10 && !design.strict) { 800 return icalValues.date.toICAL(aValue); 801 } else if (len >= 19) { 802 var result = aValue.substr(0, 4) + 803 aValue.substr(5, 2) + 804 // grab the (DDTHH) segment 805 aValue.substr(8, 5) + 806 // MM 807 aValue.substr(14, 2) + 808 // SS 809 aValue.substr(17, 2); 810 811 if (aValue[19] && aValue[19] === 'Z') { 812 result += 'Z'; 813 } 814 return result; 815 } else { 816 // TODO: error 817 return aValue; 818 } 819 }, 820 821 decorate: function(aValue, aProp) { 822 if (design.strict) { 823 return ICAL.Time.fromDateTimeString(aValue, aProp); 824 } else { 825 return ICAL.Time.fromString(aValue, aProp); 826 } 827 }, 828 829 undecorate: function(aValue) { 830 return aValue.toString(); 831 } 832 }, 833 duration: { 834 decorate: function(aValue) { 835 return ICAL.Duration.fromString(aValue); 836 }, 837 undecorate: function(aValue) { 838 return aValue.toString(); 839 } 840 }, 841 period: { 842 843 fromICAL: function(string) { 844 var parts = string.split('/'); 845 parts[0] = icalValues['date-time'].fromICAL(parts[0]); 846 847 if (!ICAL.Duration.isValueString(parts[1])) { 848 parts[1] = icalValues['date-time'].fromICAL(parts[1]); 849 } 850 851 return parts; 852 }, 853 854 toICAL: function(parts) { 855 if (!design.strict && parts[0].length == 10) { 856 parts[0] = icalValues.date.toICAL(parts[0]); 857 } else { 858 parts[0] = icalValues['date-time'].toICAL(parts[0]); 859 } 860 861 if (!ICAL.Duration.isValueString(parts[1])) { 862 if (!design.strict && parts[1].length == 10) { 863 parts[1] = icalValues.date.toICAL(parts[1]); 864 } else { 865 parts[1] = icalValues['date-time'].toICAL(parts[1]); 866 } 867 } 868 869 return parts.join("/"); 870 }, 871 872 decorate: function(aValue, aProp) { 873 return ICAL.Period.fromJSON(aValue, aProp, !design.strict); 874 }, 875 876 undecorate: function(aValue) { 877 return aValue.toJSON(); 878 } 879 }, 880 recur: { 881 fromICAL: function(string) { 882 return ICAL.Recur._stringToData(string, true); 883 }, 884 885 toICAL: function(data) { 886 var str = ""; 887 for (var k in data) { 888 /* istanbul ignore if */ 889 if (!Object.prototype.hasOwnProperty.call(data, k)) { 890 continue; 891 } 892 var val = data[k]; 893 if (k == "until") { 894 if (val.length > 10) { 895 val = icalValues['date-time'].toICAL(val); 896 } else { 897 val = icalValues.date.toICAL(val); 898 } 899 } else if (k == "wkst") { 900 if (typeof val === 'number') { 901 val = ICAL.Recur.numericDayToIcalDay(val); 902 } 903 } else if (Array.isArray(val)) { 904 val = val.join(","); 905 } 906 str += k.toUpperCase() + "=" + val + ";"; 907 } 908 return str.substr(0, str.length - 1); 909 }, 910 911 decorate: function decorate(aValue) { 912 return ICAL.Recur.fromData(aValue); 913 }, 914 915 undecorate: function(aRecur) { 916 return aRecur.toJSON(); 917 } 918 }, 919 920 time: { 921 fromICAL: function(aValue) { 922 // from: MMHHSS(Z)? 923 // to: HH:MM:SS(Z)? 924 if (aValue.length < 6) { 925 // TODO: parser exception? 926 return aValue; 927 } 928 929 // HH::MM::SSZ? 930 var result = aValue.substr(0, 2) + ':' + 931 aValue.substr(2, 2) + ':' + 932 aValue.substr(4, 2); 933 934 if (aValue[6] === 'Z') { 935 result += 'Z'; 936 } 937 938 return result; 939 }, 940 941 toICAL: function(aValue) { 942 // from: HH:MM:SS(Z)? 943 // to: MMHHSS(Z)? 944 if (aValue.length < 8) { 945 //TODO: error 946 return aValue; 947 } 948 949 var result = aValue.substr(0, 2) + 950 aValue.substr(3, 2) + 951 aValue.substr(6, 2); 952 953 if (aValue[8] === 'Z') { 954 result += 'Z'; 955 } 956 957 return result; 958 } 959 } 960 }); 961 962 var icalProperties = ICAL.helpers.extend(commonProperties, { 963 964 "action": DEFAULT_TYPE_TEXT, 965 "attach": { defaultType: "uri" }, 966 "attendee": { defaultType: "cal-address" }, 967 "calscale": DEFAULT_TYPE_TEXT, 968 "class": DEFAULT_TYPE_TEXT, 969 "comment": DEFAULT_TYPE_TEXT, 970 "completed": DEFAULT_TYPE_DATETIME, 971 "contact": DEFAULT_TYPE_TEXT, 972 "created": DEFAULT_TYPE_DATETIME, 973 "description": DEFAULT_TYPE_TEXT, 974 "dtend": DEFAULT_TYPE_DATETIME_DATE, 975 "dtstamp": DEFAULT_TYPE_DATETIME, 976 "dtstart": DEFAULT_TYPE_DATETIME_DATE, 977 "due": DEFAULT_TYPE_DATETIME_DATE, 978 "duration": { defaultType: "duration" }, 979 "exdate": { 980 defaultType: "date-time", 981 allowedTypes: ["date-time", "date"], 982 multiValue: ',' 983 }, 984 "exrule": DEFAULT_TYPE_RECUR, 985 "freebusy": { defaultType: "period", multiValue: "," }, 986 "geo": { defaultType: "float", structuredValue: ";" }, 987 "last-modified": DEFAULT_TYPE_DATETIME, 988 "location": DEFAULT_TYPE_TEXT, 989 "method": DEFAULT_TYPE_TEXT, 990 "organizer": { defaultType: "cal-address" }, 991 "percent-complete": DEFAULT_TYPE_INTEGER, 992 "priority": DEFAULT_TYPE_INTEGER, 993 "prodid": DEFAULT_TYPE_TEXT, 994 "related-to": DEFAULT_TYPE_TEXT, 995 "repeat": DEFAULT_TYPE_INTEGER, 996 "rdate": { 997 defaultType: "date-time", 998 allowedTypes: ["date-time", "date", "period"], 999 multiValue: ',', 1000 detectType: function(string) { 1001 if (string.indexOf('/') !== -1) { 1002 return 'period'; 1003 } 1004 return (string.indexOf('T') === -1) ? 'date' : 'date-time'; 1005 } 1006 }, 1007 "recurrence-id": DEFAULT_TYPE_DATETIME_DATE, 1008 "resources": DEFAULT_TYPE_TEXT_MULTI, 1009 "request-status": DEFAULT_TYPE_TEXT_STRUCTURED, 1010 "rrule": DEFAULT_TYPE_RECUR, 1011 "sequence": DEFAULT_TYPE_INTEGER, 1012 "status": DEFAULT_TYPE_TEXT, 1013 "summary": DEFAULT_TYPE_TEXT, 1014 "transp": DEFAULT_TYPE_TEXT, 1015 "trigger": { defaultType: "duration", allowedTypes: ["duration", "date-time"] }, 1016 "tzoffsetfrom": DEFAULT_TYPE_UTCOFFSET, 1017 "tzoffsetto": DEFAULT_TYPE_UTCOFFSET, 1018 "tzurl": DEFAULT_TYPE_URI, 1019 "tzid": DEFAULT_TYPE_TEXT, 1020 "tzname": DEFAULT_TYPE_TEXT 1021 }); 1022 1023 // When adding a value here, be sure to add it to the parameter types! 1024 var vcardValues = ICAL.helpers.extend(commonValues, { 1025 text: createTextType(FROM_VCARD_NEWLINE, TO_VCARD_NEWLINE), 1026 uri: createTextType(FROM_VCARD_NEWLINE, TO_VCARD_NEWLINE), 1027 1028 date: { 1029 decorate: function(aValue) { 1030 return ICAL.VCardTime.fromDateAndOrTimeString(aValue, "date"); 1031 }, 1032 undecorate: function(aValue) { 1033 return aValue.toString(); 1034 }, 1035 fromICAL: function(aValue) { 1036 if (aValue.length == 8) { 1037 return icalValues.date.fromICAL(aValue); 1038 } else if (aValue[0] == '-' && aValue.length == 6) { 1039 return aValue.substr(0, 4) + '-' + aValue.substr(4); 1040 } else { 1041 return aValue; 1042 } 1043 }, 1044 toICAL: function(aValue) { 1045 if (aValue.length == 10) { 1046 return icalValues.date.toICAL(aValue); 1047 } else if (aValue[0] == '-' && aValue.length == 7) { 1048 return aValue.substr(0, 4) + aValue.substr(5); 1049 } else { 1050 return aValue; 1051 } 1052 } 1053 }, 1054 1055 time: { 1056 decorate: function(aValue) { 1057 return ICAL.VCardTime.fromDateAndOrTimeString("T" + aValue, "time"); 1058 }, 1059 undecorate: function(aValue) { 1060 return aValue.toString(); 1061 }, 1062 fromICAL: function(aValue) { 1063 var splitzone = vcardValues.time._splitZone(aValue, true); 1064 var zone = splitzone[0], value = splitzone[1]; 1065 1066 //console.log("SPLIT: ",splitzone); 1067 1068 if (value.length == 6) { 1069 value = value.substr(0, 2) + ':' + 1070 value.substr(2, 2) + ':' + 1071 value.substr(4, 2); 1072 } else if (value.length == 4 && value[0] != '-') { 1073 value = value.substr(0, 2) + ':' + value.substr(2, 2); 1074 } else if (value.length == 5) { 1075 value = value.substr(0, 3) + ':' + value.substr(3, 2); 1076 } 1077 1078 if (zone.length == 5 && (zone[0] == '-' || zone[0] == '+')) { 1079 zone = zone.substr(0, 3) + ':' + zone.substr(3); 1080 } 1081 1082 return value + zone; 1083 }, 1084 1085 toICAL: function(aValue) { 1086 var splitzone = vcardValues.time._splitZone(aValue); 1087 var zone = splitzone[0], value = splitzone[1]; 1088 1089 if (value.length == 8) { 1090 value = value.substr(0, 2) + 1091 value.substr(3, 2) + 1092 value.substr(6, 2); 1093 } else if (value.length == 5 && value[0] != '-') { 1094 value = value.substr(0, 2) + value.substr(3, 2); 1095 } else if (value.length == 6) { 1096 value = value.substr(0, 3) + value.substr(4, 2); 1097 } 1098 1099 if (zone.length == 6 && (zone[0] == '-' || zone[0] == '+')) { 1100 zone = zone.substr(0, 3) + zone.substr(4); 1101 } 1102 1103 return value + zone; 1104 }, 1105 1106 _splitZone: function(aValue, isFromIcal) { 1107 var lastChar = aValue.length - 1; 1108 var signChar = aValue.length - (isFromIcal ? 5 : 6); 1109 var sign = aValue[signChar]; 1110 var zone, value; 1111 1112 if (aValue[lastChar] == 'Z') { 1113 zone = aValue[lastChar]; 1114 value = aValue.substr(0, lastChar); 1115 } else if (aValue.length > 6 && (sign == '-' || sign == '+')) { 1116 zone = aValue.substr(signChar); 1117 value = aValue.substr(0, signChar); 1118 } else { 1119 zone = ""; 1120 value = aValue; 1121 } 1122 1123 return [zone, value]; 1124 } 1125 }, 1126 1127 "date-time": { 1128 decorate: function(aValue) { 1129 return ICAL.VCardTime.fromDateAndOrTimeString(aValue, "date-time"); 1130 }, 1131 1132 undecorate: function(aValue) { 1133 return aValue.toString(); 1134 }, 1135 1136 fromICAL: function(aValue) { 1137 return vcardValues['date-and-or-time'].fromICAL(aValue); 1138 }, 1139 1140 toICAL: function(aValue) { 1141 return vcardValues['date-and-or-time'].toICAL(aValue); 1142 } 1143 }, 1144 1145 "date-and-or-time": { 1146 decorate: function(aValue) { 1147 return ICAL.VCardTime.fromDateAndOrTimeString(aValue, "date-and-or-time"); 1148 }, 1149 1150 undecorate: function(aValue) { 1151 return aValue.toString(); 1152 }, 1153 1154 fromICAL: function(aValue) { 1155 var parts = aValue.split('T'); 1156 return (parts[0] ? vcardValues.date.fromICAL(parts[0]) : '') + 1157 (parts[1] ? 'T' + vcardValues.time.fromICAL(parts[1]) : ''); 1158 }, 1159 1160 toICAL: function(aValue) { 1161 var parts = aValue.split('T'); 1162 return vcardValues.date.toICAL(parts[0]) + 1163 (parts[1] ? 'T' + vcardValues.time.toICAL(parts[1]) : ''); 1164 1165 } 1166 }, 1167 timestamp: icalValues['date-time'], 1168 "language-tag": { 1169 matches: /^[a-zA-Z0-9-]+$/ // Could go with a more strict regex here 1170 } 1171 }); 1172 1173 var vcardParams = { 1174 "type": { 1175 valueType: "text", 1176 multiValue: "," 1177 }, 1178 "value": { 1179 // since the value here is a 'type' lowercase is used. 1180 values: ["text", "uri", "date", "time", "date-time", "date-and-or-time", 1181 "timestamp", "boolean", "integer", "float", "utc-offset", 1182 "language-tag"], 1183 allowXName: true, 1184 allowIanaToken: true 1185 } 1186 }; 1187 1188 var vcardProperties = ICAL.helpers.extend(commonProperties, { 1189 "adr": { defaultType: "text", structuredValue: ";", multiValue: "," }, 1190 "anniversary": DEFAULT_TYPE_DATE_ANDOR_TIME, 1191 "bday": DEFAULT_TYPE_DATE_ANDOR_TIME, 1192 "caladruri": DEFAULT_TYPE_URI, 1193 "caluri": DEFAULT_TYPE_URI, 1194 "clientpidmap": DEFAULT_TYPE_TEXT_STRUCTURED, 1195 "email": DEFAULT_TYPE_TEXT, 1196 "fburl": DEFAULT_TYPE_URI, 1197 "fn": DEFAULT_TYPE_TEXT, 1198 "gender": DEFAULT_TYPE_TEXT_STRUCTURED, 1199 "geo": DEFAULT_TYPE_URI, 1200 "impp": DEFAULT_TYPE_URI, 1201 "key": DEFAULT_TYPE_URI, 1202 "kind": DEFAULT_TYPE_TEXT, 1203 "lang": { defaultType: "language-tag" }, 1204 "logo": DEFAULT_TYPE_URI, 1205 "member": DEFAULT_TYPE_URI, 1206 "n": { defaultType: "text", structuredValue: ";", multiValue: "," }, 1207 "nickname": DEFAULT_TYPE_TEXT_MULTI, 1208 "note": DEFAULT_TYPE_TEXT, 1209 "org": { defaultType: "text", structuredValue: ";" }, 1210 "photo": DEFAULT_TYPE_URI, 1211 "related": DEFAULT_TYPE_URI, 1212 "rev": { defaultType: "timestamp" }, 1213 "role": DEFAULT_TYPE_TEXT, 1214 "sound": DEFAULT_TYPE_URI, 1215 "source": DEFAULT_TYPE_URI, 1216 "tel": { defaultType: "uri", allowedTypes: ["uri", "text"] }, 1217 "title": DEFAULT_TYPE_TEXT, 1218 "tz": { defaultType: "text", allowedTypes: ["text", "utc-offset", "uri"] }, 1219 "xml": DEFAULT_TYPE_TEXT 1220 }); 1221 1222 var vcard3Values = ICAL.helpers.extend(commonValues, { 1223 binary: icalValues.binary, 1224 date: vcardValues.date, 1225 "date-time": vcardValues["date-time"], 1226 "phone-number": { 1227 // TODO 1228 /* ... */ 1229 }, 1230 uri: icalValues.uri, 1231 text: icalValues.text, 1232 time: icalValues.time, 1233 vcard: icalValues.text, 1234 "utc-offset": { 1235 toICAL: function(aValue) { 1236 return aValue.substr(0, 7); 1237 }, 1238 1239 fromICAL: function(aValue) { 1240 return aValue.substr(0, 7); 1241 }, 1242 1243 decorate: function(aValue) { 1244 return ICAL.UtcOffset.fromString(aValue); 1245 }, 1246 1247 undecorate: function(aValue) { 1248 return aValue.toString(); 1249 } 1250 } 1251 }); 1252 1253 var vcard3Params = { 1254 "type": { 1255 valueType: "text", 1256 multiValue: "," 1257 }, 1258 "value": { 1259 // since the value here is a 'type' lowercase is used. 1260 values: ["text", "uri", "date", "date-time", "phone-number", "time", 1261 "boolean", "integer", "float", "utc-offset", "vcard", "binary"], 1262 allowXName: true, 1263 allowIanaToken: true 1264 } 1265 }; 1266 1267 var vcard3Properties = ICAL.helpers.extend(commonProperties, { 1268 fn: DEFAULT_TYPE_TEXT, 1269 n: { defaultType: "text", structuredValue: ";", multiValue: "," }, 1270 nickname: DEFAULT_TYPE_TEXT_MULTI, 1271 photo: { defaultType: "binary", allowedTypes: ["binary", "uri"] }, 1272 bday: { 1273 defaultType: "date-time", 1274 allowedTypes: ["date-time", "date"], 1275 detectType: function(string) { 1276 return (string.indexOf('T') === -1) ? 'date' : 'date-time'; 1277 } 1278 }, 1279 1280 adr: { defaultType: "text", structuredValue: ";", multiValue: "," }, 1281 label: DEFAULT_TYPE_TEXT, 1282 1283 tel: { defaultType: "phone-number" }, 1284 email: DEFAULT_TYPE_TEXT, 1285 mailer: DEFAULT_TYPE_TEXT, 1286 1287 tz: { defaultType: "utc-offset", allowedTypes: ["utc-offset", "text"] }, 1288 geo: { defaultType: "float", structuredValue: ";" }, 1289 1290 title: DEFAULT_TYPE_TEXT, 1291 role: DEFAULT_TYPE_TEXT, 1292 logo: { defaultType: "binary", allowedTypes: ["binary", "uri"] }, 1293 agent: { defaultType: "vcard", allowedTypes: ["vcard", "text", "uri"] }, 1294 org: DEFAULT_TYPE_TEXT_STRUCTURED, 1295 1296 note: DEFAULT_TYPE_TEXT_MULTI, 1297 prodid: DEFAULT_TYPE_TEXT, 1298 rev: { 1299 defaultType: "date-time", 1300 allowedTypes: ["date-time", "date"], 1301 detectType: function(string) { 1302 return (string.indexOf('T') === -1) ? 'date' : 'date-time'; 1303 } 1304 }, 1305 "sort-string": DEFAULT_TYPE_TEXT, 1306 sound: { defaultType: "binary", allowedTypes: ["binary", "uri"] }, 1307 1308 class: DEFAULT_TYPE_TEXT, 1309 key: { defaultType: "binary", allowedTypes: ["binary", "text"] } 1310 }); 1311 1312 /** 1313 * iCalendar design set 1314 * @type {ICAL.design.designSet} 1315 */ 1316 var icalSet = { 1317 value: icalValues, 1318 param: icalParams, 1319 property: icalProperties 1320 }; 1321 1322 /** 1323 * vCard 4.0 design set 1324 * @type {ICAL.design.designSet} 1325 */ 1326 var vcardSet = { 1327 value: vcardValues, 1328 param: vcardParams, 1329 property: vcardProperties 1330 }; 1331 1332 /** 1333 * vCard 3.0 design set 1334 * @type {ICAL.design.designSet} 1335 */ 1336 var vcard3Set = { 1337 value: vcard3Values, 1338 param: vcard3Params, 1339 property: vcard3Properties 1340 }; 1341 1342 /** 1343 * The design data, used by the parser to determine types for properties and 1344 * other metadata needed to produce correct jCard/jCal data. 1345 * 1346 * @alias ICAL.design 1347 * @namespace 1348 */ 1349 var design = { 1350 /** 1351 * A designSet describes value, parameter and property data. It is used by 1352 * ther parser and stringifier in components and properties to determine they 1353 * should be represented. 1354 * 1355 * @typedef {Object} designSet 1356 * @memberOf ICAL.design 1357 * @property {Object} value Definitions for value types, keys are type names 1358 * @property {Object} param Definitions for params, keys are param names 1359 * @property {Object} property Defintions for properties, keys are property names 1360 */ 1361 1362 /** 1363 * Can be set to false to make the parser more lenient. 1364 */ 1365 strict: true, 1366 1367 /** 1368 * The default set for new properties and components if none is specified. 1369 * @type {ICAL.design.designSet} 1370 */ 1371 defaultSet: icalSet, 1372 1373 /** 1374 * The default type for unknown properties 1375 * @type {String} 1376 */ 1377 defaultType: 'unknown', 1378 1379 /** 1380 * Holds the design set for known top-level components 1381 * 1382 * @type {Object} 1383 * @property {ICAL.design.designSet} vcard vCard VCARD 1384 * @property {ICAL.design.designSet} vevent iCalendar VEVENT 1385 * @property {ICAL.design.designSet} vtodo iCalendar VTODO 1386 * @property {ICAL.design.designSet} vjournal iCalendar VJOURNAL 1387 * @property {ICAL.design.designSet} valarm iCalendar VALARM 1388 * @property {ICAL.design.designSet} vtimezone iCalendar VTIMEZONE 1389 * @property {ICAL.design.designSet} daylight iCalendar DAYLIGHT 1390 * @property {ICAL.design.designSet} standard iCalendar STANDARD 1391 * 1392 * @example 1393 * var propertyName = 'fn'; 1394 * var componentDesign = ICAL.design.components.vcard; 1395 * var propertyDetails = componentDesign.property[propertyName]; 1396 * if (propertyDetails.defaultType == 'text') { 1397 * // Yep, sure is... 1398 * } 1399 */ 1400 components: { 1401 vcard: vcardSet, 1402 vcard3: vcard3Set, 1403 vevent: icalSet, 1404 vtodo: icalSet, 1405 vjournal: icalSet, 1406 valarm: icalSet, 1407 vtimezone: icalSet, 1408 daylight: icalSet, 1409 standard: icalSet 1410 }, 1411 1412 1413 /** 1414 * The design set for iCalendar (rfc5545/rfc7265) components. 1415 * @type {ICAL.design.designSet} 1416 */ 1417 icalendar: icalSet, 1418 1419 /** 1420 * The design set for vCard (rfc6350/rfc7095) components. 1421 * @type {ICAL.design.designSet} 1422 */ 1423 vcard: vcardSet, 1424 1425 /** 1426 * The design set for vCard (rfc2425/rfc2426/rfc7095) components. 1427 * @type {ICAL.design.designSet} 1428 */ 1429 vcard3: vcard3Set, 1430 1431 /** 1432 * Gets the design set for the given component name. 1433 * 1434 * @param {String} componentName The name of the component 1435 * @return {ICAL.design.designSet} The design set for the component 1436 */ 1437 getDesignSet: function(componentName) { 1438 var isInDesign = componentName && componentName in design.components; 1439 return isInDesign ? design.components[componentName] : design.defaultSet; 1440 } 1441 }; 1442 1443 return design; 1444}()); 1445/* This Source Code Form is subject to the terms of the Mozilla Public 1446 * License, v. 2.0. If a copy of the MPL was not distributed with this 1447 * file, You can obtain one at http://mozilla.org/MPL/2.0/. 1448 * Portions Copyright (C) Philipp Kewisch, 2011-2015 */ 1449 1450 1451/** 1452 * Contains various functions to convert jCal and jCard data back into 1453 * iCalendar and vCard. 1454 * @namespace 1455 */ 1456ICAL.stringify = (function() { 1457 'use strict'; 1458 1459 var LINE_ENDING = '\r\n'; 1460 var DEFAULT_VALUE_TYPE = 'unknown'; 1461 1462 var design = ICAL.design; 1463 var helpers = ICAL.helpers; 1464 1465 /** 1466 * Convert a full jCal/jCard array into a iCalendar/vCard string. 1467 * 1468 * @function ICAL.stringify 1469 * @variation function 1470 * @param {Array} jCal The jCal/jCard document 1471 * @return {String} The stringified iCalendar/vCard document 1472 */ 1473 function stringify(jCal) { 1474 if (typeof jCal[0] == "string") { 1475 // This is a single component 1476 jCal = [jCal]; 1477 } 1478 1479 var i = 0; 1480 var len = jCal.length; 1481 var result = ''; 1482 1483 for (; i < len; i++) { 1484 result += stringify.component(jCal[i]) + LINE_ENDING; 1485 } 1486 1487 return result; 1488 } 1489 1490 /** 1491 * Converts an jCal component array into a ICAL string. 1492 * Recursive will resolve sub-components. 1493 * 1494 * Exact component/property order is not saved all 1495 * properties will come before subcomponents. 1496 * 1497 * @function ICAL.stringify.component 1498 * @param {Array} component 1499 * jCal/jCard fragment of a component 1500 * @param {ICAL.design.designSet} designSet 1501 * The design data to use for this component 1502 * @return {String} The iCalendar/vCard string 1503 */ 1504 stringify.component = function(component, designSet) { 1505 var name = component[0].toUpperCase(); 1506 var result = 'BEGIN:' + name + LINE_ENDING; 1507 1508 var props = component[1]; 1509 var propIdx = 0; 1510 var propLen = props.length; 1511 1512 var designSetName = component[0]; 1513 // rfc6350 requires that in vCard 4.0 the first component is the VERSION 1514 // component with as value 4.0, note that 3.0 does not have this requirement. 1515 if (designSetName === 'vcard' && component[1].length > 0 && 1516 !(component[1][0][0] === "version" && component[1][0][3] === "4.0")) { 1517 designSetName = "vcard3"; 1518 } 1519 designSet = designSet || design.getDesignSet(designSetName); 1520 1521 for (; propIdx < propLen; propIdx++) { 1522 result += stringify.property(props[propIdx], designSet) + LINE_ENDING; 1523 } 1524 1525 // Ignore subcomponents if none exist, e.g. in vCard. 1526 var comps = component[2] || []; 1527 var compIdx = 0; 1528 var compLen = comps.length; 1529 1530 for (; compIdx < compLen; compIdx++) { 1531 result += stringify.component(comps[compIdx], designSet) + LINE_ENDING; 1532 } 1533 1534 result += 'END:' + name; 1535 return result; 1536 }; 1537 1538 /** 1539 * Converts a single jCal/jCard property to a iCalendar/vCard string. 1540 * 1541 * @function ICAL.stringify.property 1542 * @param {Array} property 1543 * jCal/jCard property array 1544 * @param {ICAL.design.designSet} designSet 1545 * The design data to use for this property 1546 * @param {Boolean} noFold 1547 * If true, the line is not folded 1548 * @return {String} The iCalendar/vCard string 1549 */ 1550 stringify.property = function(property, designSet, noFold) { 1551 var name = property[0].toUpperCase(); 1552 var jsName = property[0]; 1553 var params = property[1]; 1554 1555 var line = name; 1556 1557 var paramName; 1558 for (paramName in params) { 1559 var value = params[paramName]; 1560 1561 /* istanbul ignore else */ 1562 if (params.hasOwnProperty(paramName)) { 1563 var multiValue = (paramName in designSet.param) && designSet.param[paramName].multiValue; 1564 if (multiValue && Array.isArray(value)) { 1565 if (designSet.param[paramName].multiValueSeparateDQuote) { 1566 multiValue = '"' + multiValue + '"'; 1567 } 1568 value = value.map(stringify._rfc6868Unescape); 1569 value = stringify.multiValue(value, multiValue, "unknown", null, designSet); 1570 } else { 1571 value = stringify._rfc6868Unescape(value); 1572 } 1573 1574 1575 line += ';' + paramName.toUpperCase(); 1576 line += '=' + stringify.propertyValue(value); 1577 } 1578 } 1579 1580 if (property.length === 3) { 1581 // If there are no values, we must assume a blank value 1582 return line + ':'; 1583 } 1584 1585 var valueType = property[2]; 1586 1587 if (!designSet) { 1588 designSet = design.defaultSet; 1589 } 1590 1591 var propDetails; 1592 var multiValue = false; 1593 var structuredValue = false; 1594 var isDefault = false; 1595 1596 if (jsName in designSet.property) { 1597 propDetails = designSet.property[jsName]; 1598 1599 if ('multiValue' in propDetails) { 1600 multiValue = propDetails.multiValue; 1601 } 1602 1603 if (('structuredValue' in propDetails) && Array.isArray(property[3])) { 1604 structuredValue = propDetails.structuredValue; 1605 } 1606 1607 if ('defaultType' in propDetails) { 1608 if (valueType === propDetails.defaultType) { 1609 isDefault = true; 1610 } 1611 } else { 1612 if (valueType === DEFAULT_VALUE_TYPE) { 1613 isDefault = true; 1614 } 1615 } 1616 } else { 1617 if (valueType === DEFAULT_VALUE_TYPE) { 1618 isDefault = true; 1619 } 1620 } 1621 1622 // push the VALUE property if type is not the default 1623 // for the current property. 1624 if (!isDefault) { 1625 // value will never contain ;/:/, so we don't escape it here. 1626 line += ';VALUE=' + valueType.toUpperCase(); 1627 } 1628 1629 line += ':'; 1630 1631 if (multiValue && structuredValue) { 1632 line += stringify.multiValue( 1633 property[3], structuredValue, valueType, multiValue, designSet, structuredValue 1634 ); 1635 } else if (multiValue) { 1636 line += stringify.multiValue( 1637 property.slice(3), multiValue, valueType, null, designSet, false 1638 ); 1639 } else if (structuredValue) { 1640 line += stringify.multiValue( 1641 property[3], structuredValue, valueType, null, designSet, structuredValue 1642 ); 1643 } else { 1644 line += stringify.value(property[3], valueType, designSet, false); 1645 } 1646 1647 return noFold ? line : ICAL.helpers.foldline(line); 1648 }; 1649 1650 /** 1651 * Handles escaping of property values that may contain: 1652 * 1653 * COLON (:), SEMICOLON (;), or COMMA (,) 1654 * 1655 * If any of the above are present the result is wrapped 1656 * in double quotes. 1657 * 1658 * @function ICAL.stringify.propertyValue 1659 * @param {String} value Raw property value 1660 * @return {String} Given or escaped value when needed 1661 */ 1662 stringify.propertyValue = function(value) { 1663 1664 if ((helpers.unescapedIndexOf(value, ',') === -1) && 1665 (helpers.unescapedIndexOf(value, ':') === -1) && 1666 (helpers.unescapedIndexOf(value, ';') === -1)) { 1667 1668 return value; 1669 } 1670 1671 return '"' + value + '"'; 1672 }; 1673 1674 /** 1675 * Converts an array of ical values into a single 1676 * string based on a type and a delimiter value (like ","). 1677 * 1678 * @function ICAL.stringify.multiValue 1679 * @param {Array} values List of values to convert 1680 * @param {String} delim Used to join the values (",", ";", ":") 1681 * @param {String} type Lowecase ical value type 1682 * (like boolean, date-time, etc..) 1683 * @param {?String} innerMulti If set, each value will again be processed 1684 * Used for structured values 1685 * @param {ICAL.design.designSet} designSet 1686 * The design data to use for this property 1687 * 1688 * @return {String} iCalendar/vCard string for value 1689 */ 1690 stringify.multiValue = function(values, delim, type, innerMulti, designSet, structuredValue) { 1691 var result = ''; 1692 var len = values.length; 1693 var i = 0; 1694 1695 for (; i < len; i++) { 1696 if (innerMulti && Array.isArray(values[i])) { 1697 result += stringify.multiValue(values[i], innerMulti, type, null, designSet, structuredValue); 1698 } else { 1699 result += stringify.value(values[i], type, designSet, structuredValue); 1700 } 1701 1702 if (i !== (len - 1)) { 1703 result += delim; 1704 } 1705 } 1706 1707 return result; 1708 }; 1709 1710 /** 1711 * Processes a single ical value runs the associated "toICAL" method from the 1712 * design value type if available to convert the value. 1713 * 1714 * @function ICAL.stringify.value 1715 * @param {String|Number} value A formatted value 1716 * @param {String} type Lowercase iCalendar/vCard value type 1717 * (like boolean, date-time, etc..) 1718 * @return {String} iCalendar/vCard value for single value 1719 */ 1720 stringify.value = function(value, type, designSet, structuredValue) { 1721 if (type in designSet.value && 'toICAL' in designSet.value[type]) { 1722 return designSet.value[type].toICAL(value, structuredValue); 1723 } 1724 return value; 1725 }; 1726 1727 /** 1728 * Internal helper for rfc6868. Exposing this on ICAL.stringify so that 1729 * hackers can disable the rfc6868 parsing if the really need to. 1730 * 1731 * @param {String} val The value to unescape 1732 * @return {String} The escaped value 1733 */ 1734 stringify._rfc6868Unescape = function(val) { 1735 return val.replace(/[\n^"]/g, function(x) { 1736 return RFC6868_REPLACE_MAP[x]; 1737 }); 1738 }; 1739 var RFC6868_REPLACE_MAP = { '"': "^'", "\n": "^n", "^": "^^" }; 1740 1741 return stringify; 1742}()); 1743/* This Source Code Form is subject to the terms of the Mozilla Public 1744 * License, v. 2.0. If a copy of the MPL was not distributed with this 1745 * file, You can obtain one at http://mozilla.org/MPL/2.0/. 1746 * Portions Copyright (C) Philipp Kewisch, 2011-2015 */ 1747 1748 1749/** 1750 * Contains various functions to parse iCalendar and vCard data. 1751 * @namespace 1752 */ 1753ICAL.parse = (function() { 1754 'use strict'; 1755 1756 var CHAR = /[^ \t]/; 1757 var MULTIVALUE_DELIMITER = ','; 1758 var VALUE_DELIMITER = ':'; 1759 var PARAM_DELIMITER = ';'; 1760 var PARAM_NAME_DELIMITER = '='; 1761 var DEFAULT_VALUE_TYPE = 'unknown'; 1762 var DEFAULT_PARAM_TYPE = 'text'; 1763 1764 var design = ICAL.design; 1765 var helpers = ICAL.helpers; 1766 1767 /** 1768 * An error that occurred during parsing. 1769 * 1770 * @param {String} message The error message 1771 * @memberof ICAL.parse 1772 * @extends {Error} 1773 * @class 1774 */ 1775 function ParserError(message) { 1776 this.message = message; 1777 this.name = 'ParserError'; 1778 1779 try { 1780 throw new Error(); 1781 } catch (e) { 1782 if (e.stack) { 1783 var split = e.stack.split('\n'); 1784 split.shift(); 1785 this.stack = split.join('\n'); 1786 } 1787 } 1788 } 1789 1790 ParserError.prototype = Error.prototype; 1791 1792 /** 1793 * Parses iCalendar or vCard data into a raw jCal object. Consult 1794 * documentation on the {@tutorial layers|layers of parsing} for more 1795 * details. 1796 * 1797 * @function ICAL.parse 1798 * @variation function 1799 * @todo Fix the API to be more clear on the return type 1800 * @param {String} input The string data to parse 1801 * @return {Object|Object[]} A single jCal object, or an array thereof 1802 */ 1803 function parser(input) { 1804 var state = {}; 1805 var root = state.component = []; 1806 1807 state.stack = [root]; 1808 1809 parser._eachLine(input, function(err, line) { 1810 parser._handleContentLine(line, state); 1811 }); 1812 1813 1814 // when there are still items on the stack 1815 // throw a fatal error, a component was not closed 1816 // correctly in that case. 1817 if (state.stack.length > 1) { 1818 throw new ParserError( 1819 'invalid ical body. component began but did not end' 1820 ); 1821 } 1822 1823 state = null; 1824 1825 return (root.length == 1 ? root[0] : root); 1826 } 1827 1828 /** 1829 * Parse an iCalendar property value into the jCal for a single property 1830 * 1831 * @function ICAL.parse.property 1832 * @param {String} str 1833 * The iCalendar property string to parse 1834 * @param {ICAL.design.designSet=} designSet 1835 * The design data to use for this property 1836 * @return {Object} 1837 * The jCal Object containing the property 1838 */ 1839 parser.property = function(str, designSet) { 1840 var state = { 1841 component: [[], []], 1842 designSet: designSet || design.defaultSet 1843 }; 1844 parser._handleContentLine(str, state); 1845 return state.component[1][0]; 1846 }; 1847 1848 /** 1849 * Convenience method to parse a component. You can use ICAL.parse() directly 1850 * instead. 1851 * 1852 * @function ICAL.parse.component 1853 * @see ICAL.parse(function) 1854 * @param {String} str The iCalendar component string to parse 1855 * @return {Object} The jCal Object containing the component 1856 */ 1857 parser.component = function(str) { 1858 return parser(str); 1859 }; 1860 1861 // classes & constants 1862 parser.ParserError = ParserError; 1863 1864 /** 1865 * The state for parsing content lines from an iCalendar/vCard string. 1866 * 1867 * @private 1868 * @memberof ICAL.parse 1869 * @typedef {Object} parserState 1870 * @property {ICAL.design.designSet} designSet The design set to use for parsing 1871 * @property {ICAL.Component[]} stack The stack of components being processed 1872 * @property {ICAL.Component} component The currently active component 1873 */ 1874 1875 1876 /** 1877 * Handles a single line of iCalendar/vCard, updating the state. 1878 * 1879 * @private 1880 * @function ICAL.parse._handleContentLine 1881 * @param {String} line The content line to process 1882 * @param {ICAL.parse.parserState} The current state of the line parsing 1883 */ 1884 parser._handleContentLine = function(line, state) { 1885 // break up the parts of the line 1886 var valuePos = line.indexOf(VALUE_DELIMITER); 1887 var paramPos = line.indexOf(PARAM_DELIMITER); 1888 1889 var lastParamIndex; 1890 var lastValuePos; 1891 1892 // name of property or begin/end 1893 var name; 1894 var value; 1895 // params is only overridden if paramPos !== -1. 1896 // we can't do params = params || {} later on 1897 // because it sacrifices ops. 1898 var params = {}; 1899 1900 /** 1901 * Different property cases 1902 * 1903 * 1904 * 1. RRULE:FREQ=foo 1905 * // FREQ= is not a param but the value 1906 * 1907 * 2. ATTENDEE;ROLE=REQ-PARTICIPANT; 1908 * // ROLE= is a param because : has not happened yet 1909 */ 1910 // when the parameter delimiter is after the 1911 // value delimiter then its not a parameter. 1912 1913 if ((paramPos !== -1 && valuePos !== -1)) { 1914 // when the parameter delimiter is after the 1915 // value delimiter then its not a parameter. 1916 if (paramPos > valuePos) { 1917 paramPos = -1; 1918 } 1919 } 1920 1921 var parsedParams; 1922 if (paramPos !== -1) { 1923 name = line.substring(0, paramPos).toLowerCase(); 1924 parsedParams = parser._parseParameters(line.substring(paramPos), 0, state.designSet); 1925 if (parsedParams[2] == -1) { 1926 throw new ParserError("Invalid parameters in '" + line + "'"); 1927 } 1928 params = parsedParams[0]; 1929 lastParamIndex = parsedParams[1].length + parsedParams[2] + paramPos; 1930 if ((lastValuePos = 1931 line.substring(lastParamIndex).indexOf(VALUE_DELIMITER)) !== -1) { 1932 value = line.substring(lastParamIndex + lastValuePos + 1); 1933 } else { 1934 throw new ParserError("Missing parameter value in '" + line + "'"); 1935 } 1936 } else if (valuePos !== -1) { 1937 // without parmeters (BEGIN:VCAENDAR, CLASS:PUBLIC) 1938 name = line.substring(0, valuePos).toLowerCase(); 1939 value = line.substring(valuePos + 1); 1940 1941 if (name === 'begin') { 1942 var newComponent = [value.toLowerCase(), [], []]; 1943 if (state.stack.length === 1) { 1944 state.component.push(newComponent); 1945 } else { 1946 state.component[2].push(newComponent); 1947 } 1948 state.stack.push(state.component); 1949 state.component = newComponent; 1950 if (!state.designSet) { 1951 state.designSet = design.getDesignSet(state.component[0]); 1952 } 1953 return; 1954 } else if (name === 'end') { 1955 state.component = state.stack.pop(); 1956 return; 1957 } 1958 // If its not begin/end, then this is a property with an empty value, 1959 // which should be considered valid. 1960 } else { 1961 /** 1962 * Invalid line. 1963 * The rational to throw an error is we will 1964 * never be certain that the rest of the file 1965 * is sane and its unlikely that we can serialize 1966 * the result correctly either. 1967 */ 1968 throw new ParserError( 1969 'invalid line (no token ";" or ":") "' + line + '"' 1970 ); 1971 } 1972 1973 var valueType; 1974 var multiValue = false; 1975 var structuredValue = false; 1976 var propertyDetails; 1977 1978 if (name in state.designSet.property) { 1979 propertyDetails = state.designSet.property[name]; 1980 1981 if ('multiValue' in propertyDetails) { 1982 multiValue = propertyDetails.multiValue; 1983 } 1984 1985 if ('structuredValue' in propertyDetails) { 1986 structuredValue = propertyDetails.structuredValue; 1987 } 1988 1989 if (value && 'detectType' in propertyDetails) { 1990 valueType = propertyDetails.detectType(value); 1991 } 1992 } 1993 1994 // attempt to determine value 1995 if (!valueType) { 1996 if (!('value' in params)) { 1997 if (propertyDetails) { 1998 valueType = propertyDetails.defaultType; 1999 } else { 2000 valueType = DEFAULT_VALUE_TYPE; 2001 } 2002 } else { 2003 // possible to avoid this? 2004 valueType = params.value.toLowerCase(); 2005 } 2006 } 2007 2008 delete params.value; 2009 2010 /** 2011 * Note on `var result` juggling: 2012 * 2013 * I observed that building the array in pieces has adverse 2014 * effects on performance, so where possible we inline the creation. 2015 * Its a little ugly but resulted in ~2000 additional ops/sec. 2016 */ 2017 2018 var result; 2019 if (multiValue && structuredValue) { 2020 value = parser._parseMultiValue(value, structuredValue, valueType, [], multiValue, state.designSet, structuredValue); 2021 result = [name, params, valueType, value]; 2022 } else if (multiValue) { 2023 result = [name, params, valueType]; 2024 parser._parseMultiValue(value, multiValue, valueType, result, null, state.designSet, false); 2025 } else if (structuredValue) { 2026 value = parser._parseMultiValue(value, structuredValue, valueType, [], null, state.designSet, structuredValue); 2027 result = [name, params, valueType, value]; 2028 } else { 2029 value = parser._parseValue(value, valueType, state.designSet, false); 2030 result = [name, params, valueType, value]; 2031 } 2032 // rfc6350 requires that in vCard 4.0 the first component is the VERSION 2033 // component with as value 4.0, note that 3.0 does not have this requirement. 2034 if (state.component[0] === 'vcard' && state.component[1].length === 0 && 2035 !(name === 'version' && value === '4.0')) { 2036 state.designSet = design.getDesignSet("vcard3"); 2037 } 2038 state.component[1].push(result); 2039 }; 2040 2041 /** 2042 * Parse a value from the raw value into the jCard/jCal value. 2043 * 2044 * @private 2045 * @function ICAL.parse._parseValue 2046 * @param {String} value Original value 2047 * @param {String} type Type of value 2048 * @param {Object} designSet The design data to use for this value 2049 * @return {Object} varies on type 2050 */ 2051 parser._parseValue = function(value, type, designSet, structuredValue) { 2052 if (type in designSet.value && 'fromICAL' in designSet.value[type]) { 2053 return designSet.value[type].fromICAL(value, structuredValue); 2054 } 2055 return value; 2056 }; 2057 2058 /** 2059 * Parse parameters from a string to object. 2060 * 2061 * @function ICAL.parse._parseParameters 2062 * @private 2063 * @param {String} line A single unfolded line 2064 * @param {Numeric} start Position to start looking for properties 2065 * @param {Object} designSet The design data to use for this property 2066 * @return {Object} key/value pairs 2067 */ 2068 parser._parseParameters = function(line, start, designSet) { 2069 var lastParam = start; 2070 var pos = 0; 2071 var delim = PARAM_NAME_DELIMITER; 2072 var result = {}; 2073 var name, lcname; 2074 var value, valuePos = -1; 2075 var type, multiValue, mvdelim; 2076 2077 // find the next '=' sign 2078 // use lastParam and pos to find name 2079 // check if " is used if so get value from "->" 2080 // then increment pos to find next ; 2081 2082 while ((pos !== false) && 2083 (pos = helpers.unescapedIndexOf(line, delim, pos + 1)) !== -1) { 2084 2085 name = line.substr(lastParam + 1, pos - lastParam - 1); 2086 if (name.length == 0) { 2087 throw new ParserError("Empty parameter name in '" + line + "'"); 2088 } 2089 lcname = name.toLowerCase(); 2090 mvdelim = false; 2091 multiValue = false; 2092 2093 if (lcname in designSet.param && designSet.param[lcname].valueType) { 2094 type = designSet.param[lcname].valueType; 2095 } else { 2096 type = DEFAULT_PARAM_TYPE; 2097 } 2098 2099 if (lcname in designSet.param) { 2100 multiValue = designSet.param[lcname].multiValue; 2101 if (designSet.param[lcname].multiValueSeparateDQuote) { 2102 mvdelim = parser._rfc6868Escape('"' + multiValue + '"'); 2103 } 2104 } 2105 2106 var nextChar = line[pos + 1]; 2107 if (nextChar === '"') { 2108 valuePos = pos + 2; 2109 pos = helpers.unescapedIndexOf(line, '"', valuePos); 2110 if (multiValue && pos != -1) { 2111 var extendedValue = true; 2112 while (extendedValue) { 2113 if (line[pos + 1] == multiValue && line[pos + 2] == '"') { 2114 pos = helpers.unescapedIndexOf(line, '"', pos + 3); 2115 } else { 2116 extendedValue = false; 2117 } 2118 } 2119 } 2120 if (pos === -1) { 2121 throw new ParserError( 2122 'invalid line (no matching double quote) "' + line + '"' 2123 ); 2124 } 2125 value = line.substr(valuePos, pos - valuePos); 2126 lastParam = helpers.unescapedIndexOf(line, PARAM_DELIMITER, pos); 2127 if (lastParam === -1) { 2128 pos = false; 2129 } 2130 } else { 2131 valuePos = pos + 1; 2132 2133 // move to next ";" 2134 var nextPos = helpers.unescapedIndexOf(line, PARAM_DELIMITER, valuePos); 2135 var propValuePos = helpers.unescapedIndexOf(line, VALUE_DELIMITER, valuePos); 2136 if (propValuePos !== -1 && nextPos > propValuePos) { 2137 // this is a delimiter in the property value, let's stop here 2138 nextPos = propValuePos; 2139 pos = false; 2140 } else if (nextPos === -1) { 2141 // no ";" 2142 if (propValuePos === -1) { 2143 nextPos = line.length; 2144 } else { 2145 nextPos = propValuePos; 2146 } 2147 pos = false; 2148 } else { 2149 lastParam = nextPos; 2150 pos = nextPos; 2151 } 2152 2153 value = line.substr(valuePos, nextPos - valuePos); 2154 } 2155 2156 value = parser._rfc6868Escape(value); 2157 if (multiValue) { 2158 var delimiter = mvdelim || multiValue; 2159 value = parser._parseMultiValue(value, delimiter, type, [], null, designSet); 2160 } else { 2161 value = parser._parseValue(value, type, designSet); 2162 } 2163 2164 if (multiValue && (lcname in result)) { 2165 if (Array.isArray(result[lcname])) { 2166 result[lcname].push(value); 2167 } else { 2168 result[lcname] = [ 2169 result[lcname], 2170 value 2171 ]; 2172 } 2173 } else { 2174 result[lcname] = value; 2175 } 2176 } 2177 return [result, value, valuePos]; 2178 }; 2179 2180 /** 2181 * Internal helper for rfc6868. Exposing this on ICAL.parse so that 2182 * hackers can disable the rfc6868 parsing if the really need to. 2183 * 2184 * @function ICAL.parse._rfc6868Escape 2185 * @param {String} val The value to escape 2186 * @return {String} The escaped value 2187 */ 2188 parser._rfc6868Escape = function(val) { 2189 return val.replace(/\^['n^]/g, function(x) { 2190 return RFC6868_REPLACE_MAP[x]; 2191 }); 2192 }; 2193 var RFC6868_REPLACE_MAP = { "^'": '"', "^n": "\n", "^^": "^" }; 2194 2195 /** 2196 * Parse a multi value string. This function is used either for parsing 2197 * actual multi-value property's values, or for handling parameter values. It 2198 * can be used for both multi-value properties and structured value properties. 2199 * 2200 * @private 2201 * @function ICAL.parse._parseMultiValue 2202 * @param {String} buffer The buffer containing the full value 2203 * @param {String} delim The multi-value delimiter 2204 * @param {String} type The value type to be parsed 2205 * @param {Array.<?>} result The array to append results to, varies on value type 2206 * @param {String} innerMulti The inner delimiter to split each value with 2207 * @param {ICAL.design.designSet} designSet The design data for this value 2208 * @return {?|Array.<?>} Either an array of results, or the first result 2209 */ 2210 parser._parseMultiValue = function(buffer, delim, type, result, innerMulti, designSet, structuredValue) { 2211 var pos = 0; 2212 var lastPos = 0; 2213 var value; 2214 if (delim.length === 0) { 2215 return buffer; 2216 } 2217 2218 // split each piece 2219 while ((pos = helpers.unescapedIndexOf(buffer, delim, lastPos)) !== -1) { 2220 value = buffer.substr(lastPos, pos - lastPos); 2221 if (innerMulti) { 2222 value = parser._parseMultiValue(value, innerMulti, type, [], null, designSet, structuredValue); 2223 } else { 2224 value = parser._parseValue(value, type, designSet, structuredValue); 2225 } 2226 result.push(value); 2227 lastPos = pos + delim.length; 2228 } 2229 2230 // on the last piece take the rest of string 2231 value = buffer.substr(lastPos); 2232 if (innerMulti) { 2233 value = parser._parseMultiValue(value, innerMulti, type, [], null, designSet, structuredValue); 2234 } else { 2235 value = parser._parseValue(value, type, designSet, structuredValue); 2236 } 2237 result.push(value); 2238 2239 return result.length == 1 ? result[0] : result; 2240 }; 2241 2242 /** 2243 * Process a complete buffer of iCalendar/vCard data line by line, correctly 2244 * unfolding content. Each line will be processed with the given callback 2245 * 2246 * @private 2247 * @function ICAL.parse._eachLine 2248 * @param {String} buffer The buffer to process 2249 * @param {function(?String, String)} callback The callback for each line 2250 */ 2251 parser._eachLine = function(buffer, callback) { 2252 var len = buffer.length; 2253 var lastPos = buffer.search(CHAR); 2254 var pos = lastPos; 2255 var line; 2256 var firstChar; 2257 2258 var newlineOffset; 2259 2260 do { 2261 pos = buffer.indexOf('\n', lastPos) + 1; 2262 2263 if (pos > 1 && buffer[pos - 2] === '\r') { 2264 newlineOffset = 2; 2265 } else { 2266 newlineOffset = 1; 2267 } 2268 2269 if (pos === 0) { 2270 pos = len; 2271 newlineOffset = 0; 2272 } 2273 2274 firstChar = buffer[lastPos]; 2275 2276 if (firstChar === ' ' || firstChar === '\t') { 2277 // add to line 2278 line += buffer.substr( 2279 lastPos + 1, 2280 pos - lastPos - (newlineOffset + 1) 2281 ); 2282 } else { 2283 if (line) 2284 callback(null, line); 2285 // push line 2286 line = buffer.substr( 2287 lastPos, 2288 pos - lastPos - newlineOffset 2289 ); 2290 } 2291 2292 lastPos = pos; 2293 } while (pos !== len); 2294 2295 // extra ending line 2296 line = line.trim(); 2297 2298 if (line.length) 2299 callback(null, line); 2300 }; 2301 2302 return parser; 2303 2304}()); 2305/* This Source Code Form is subject to the terms of the Mozilla Public 2306 * License, v. 2.0. If a copy of the MPL was not distributed with this 2307 * file, You can obtain one at http://mozilla.org/MPL/2.0/. 2308 * Portions Copyright (C) Philipp Kewisch, 2011-2015 */ 2309 2310 2311/** 2312 * This symbol is further described later on 2313 * @ignore 2314 */ 2315ICAL.Component = (function() { 2316 'use strict'; 2317 2318 var PROPERTY_INDEX = 1; 2319 var COMPONENT_INDEX = 2; 2320 var NAME_INDEX = 0; 2321 2322 /** 2323 * @classdesc 2324 * Wraps a jCal component, adding convenience methods to add, remove and 2325 * update subcomponents and properties. 2326 * 2327 * @class 2328 * @alias ICAL.Component 2329 * @param {Array|String} jCal Raw jCal component data OR name of new 2330 * component 2331 * @param {ICAL.Component} parent Parent component to associate 2332 */ 2333 function Component(jCal, parent) { 2334 if (typeof(jCal) === 'string') { 2335 // jCal spec (name, properties, components) 2336 jCal = [jCal, [], []]; 2337 } 2338 2339 // mostly for legacy reasons. 2340 this.jCal = jCal; 2341 2342 this.parent = parent || null; 2343 } 2344 2345 Component.prototype = { 2346 /** 2347 * Hydrated properties are inserted into the _properties array at the same 2348 * position as in the jCal array, so its possible the array contains 2349 * undefined values for unhydrdated properties. To avoid iterating the 2350 * array when checking if all properties have been hydrated, we save the 2351 * count here. 2352 * 2353 * @type {Number} 2354 * @private 2355 */ 2356 _hydratedPropertyCount: 0, 2357 2358 /** 2359 * The same count as for _hydratedPropertyCount, but for subcomponents 2360 * 2361 * @type {Number} 2362 * @private 2363 */ 2364 _hydratedComponentCount: 0, 2365 2366 /** 2367 * The name of this component 2368 * @readonly 2369 */ 2370 get name() { 2371 return this.jCal[NAME_INDEX]; 2372 }, 2373 2374 /** 2375 * The design set for this component, e.g. icalendar vs vcard 2376 * 2377 * @type {ICAL.design.designSet} 2378 * @private 2379 */ 2380 get _designSet() { 2381 var parentDesign = this.parent && this.parent._designSet; 2382 return parentDesign || ICAL.design.getDesignSet(this.name); 2383 }, 2384 2385 _hydrateComponent: function(index) { 2386 if (!this._components) { 2387 this._components = []; 2388 this._hydratedComponentCount = 0; 2389 } 2390 2391 if (this._components[index]) { 2392 return this._components[index]; 2393 } 2394 2395 var comp = new Component( 2396 this.jCal[COMPONENT_INDEX][index], 2397 this 2398 ); 2399 2400 this._hydratedComponentCount++; 2401 return (this._components[index] = comp); 2402 }, 2403 2404 _hydrateProperty: function(index) { 2405 if (!this._properties) { 2406 this._properties = []; 2407 this._hydratedPropertyCount = 0; 2408 } 2409 2410 if (this._properties[index]) { 2411 return this._properties[index]; 2412 } 2413 2414 var prop = new ICAL.Property( 2415 this.jCal[PROPERTY_INDEX][index], 2416 this 2417 ); 2418 2419 this._hydratedPropertyCount++; 2420 return (this._properties[index] = prop); 2421 }, 2422 2423 /** 2424 * Finds first sub component, optionally filtered by name. 2425 * 2426 * @param {String=} name Optional name to filter by 2427 * @return {?ICAL.Component} The found subcomponent 2428 */ 2429 getFirstSubcomponent: function(name) { 2430 if (name) { 2431 var i = 0; 2432 var comps = this.jCal[COMPONENT_INDEX]; 2433 var len = comps.length; 2434 2435 for (; i < len; i++) { 2436 if (comps[i][NAME_INDEX] === name) { 2437 var result = this._hydrateComponent(i); 2438 return result; 2439 } 2440 } 2441 } else { 2442 if (this.jCal[COMPONENT_INDEX].length) { 2443 return this._hydrateComponent(0); 2444 } 2445 } 2446 2447 // ensure we return a value (strict mode) 2448 return null; 2449 }, 2450 2451 /** 2452 * Finds all sub components, optionally filtering by name. 2453 * 2454 * @param {String=} name Optional name to filter by 2455 * @return {ICAL.Component[]} The found sub components 2456 */ 2457 getAllSubcomponents: function(name) { 2458 var jCalLen = this.jCal[COMPONENT_INDEX].length; 2459 var i = 0; 2460 2461 if (name) { 2462 var comps = this.jCal[COMPONENT_INDEX]; 2463 var result = []; 2464 2465 for (; i < jCalLen; i++) { 2466 if (name === comps[i][NAME_INDEX]) { 2467 result.push( 2468 this._hydrateComponent(i) 2469 ); 2470 } 2471 } 2472 return result; 2473 } else { 2474 if (!this._components || 2475 (this._hydratedComponentCount !== jCalLen)) { 2476 for (; i < jCalLen; i++) { 2477 this._hydrateComponent(i); 2478 } 2479 } 2480 2481 return this._components || []; 2482 } 2483 }, 2484 2485 /** 2486 * Returns true when a named property exists. 2487 * 2488 * @param {String} name The property name 2489 * @return {Boolean} True, when property is found 2490 */ 2491 hasProperty: function(name) { 2492 var props = this.jCal[PROPERTY_INDEX]; 2493 var len = props.length; 2494 2495 var i = 0; 2496 for (; i < len; i++) { 2497 // 0 is property name 2498 if (props[i][NAME_INDEX] === name) { 2499 return true; 2500 } 2501 } 2502 2503 return false; 2504 }, 2505 2506 /** 2507 * Finds the first property, optionally with the given name. 2508 * 2509 * @param {String=} name Lowercase property name 2510 * @return {?ICAL.Property} The found property 2511 */ 2512 getFirstProperty: function(name) { 2513 if (name) { 2514 var i = 0; 2515 var props = this.jCal[PROPERTY_INDEX]; 2516 var len = props.length; 2517 2518 for (; i < len; i++) { 2519 if (props[i][NAME_INDEX] === name) { 2520 var result = this._hydrateProperty(i); 2521 return result; 2522 } 2523 } 2524 } else { 2525 if (this.jCal[PROPERTY_INDEX].length) { 2526 return this._hydrateProperty(0); 2527 } 2528 } 2529 2530 return null; 2531 }, 2532 2533 /** 2534 * Returns first property's value, if available. 2535 * 2536 * @param {String=} name Lowercase property name 2537 * @return {?String} The found property value. 2538 */ 2539 getFirstPropertyValue: function(name) { 2540 var prop = this.getFirstProperty(name); 2541 if (prop) { 2542 return prop.getFirstValue(); 2543 } 2544 2545 return null; 2546 }, 2547 2548 /** 2549 * Get all properties in the component, optionally filtered by name. 2550 * 2551 * @param {String=} name Lowercase property name 2552 * @return {ICAL.Property[]} List of properties 2553 */ 2554 getAllProperties: function(name) { 2555 var jCalLen = this.jCal[PROPERTY_INDEX].length; 2556 var i = 0; 2557 2558 if (name) { 2559 var props = this.jCal[PROPERTY_INDEX]; 2560 var result = []; 2561 2562 for (; i < jCalLen; i++) { 2563 if (name === props[i][NAME_INDEX]) { 2564 result.push( 2565 this._hydrateProperty(i) 2566 ); 2567 } 2568 } 2569 return result; 2570 } else { 2571 if (!this._properties || 2572 (this._hydratedPropertyCount !== jCalLen)) { 2573 for (; i < jCalLen; i++) { 2574 this._hydrateProperty(i); 2575 } 2576 } 2577 2578 return this._properties || []; 2579 } 2580 }, 2581 2582 _removeObjectByIndex: function(jCalIndex, cache, index) { 2583 cache = cache || []; 2584 // remove cached version 2585 if (cache[index]) { 2586 var obj = cache[index]; 2587 if ("parent" in obj) { 2588 obj.parent = null; 2589 } 2590 } 2591 2592 cache.splice(index, 1); 2593 2594 // remove it from the jCal 2595 this.jCal[jCalIndex].splice(index, 1); 2596 }, 2597 2598 _removeObject: function(jCalIndex, cache, nameOrObject) { 2599 var i = 0; 2600 var objects = this.jCal[jCalIndex]; 2601 var len = objects.length; 2602 var cached = this[cache]; 2603 2604 if (typeof(nameOrObject) === 'string') { 2605 for (; i < len; i++) { 2606 if (objects[i][NAME_INDEX] === nameOrObject) { 2607 this._removeObjectByIndex(jCalIndex, cached, i); 2608 return true; 2609 } 2610 } 2611 } else if (cached) { 2612 for (; i < len; i++) { 2613 if (cached[i] && cached[i] === nameOrObject) { 2614 this._removeObjectByIndex(jCalIndex, cached, i); 2615 return true; 2616 } 2617 } 2618 } 2619 2620 return false; 2621 }, 2622 2623 _removeAllObjects: function(jCalIndex, cache, name) { 2624 var cached = this[cache]; 2625 2626 // Unfortunately we have to run through all children to reset their 2627 // parent property. 2628 var objects = this.jCal[jCalIndex]; 2629 var i = objects.length - 1; 2630 2631 // descending search required because splice 2632 // is used and will effect the indices. 2633 for (; i >= 0; i--) { 2634 if (!name || objects[i][NAME_INDEX] === name) { 2635 this._removeObjectByIndex(jCalIndex, cached, i); 2636 } 2637 } 2638 }, 2639 2640 /** 2641 * Adds a single sub component. 2642 * 2643 * @param {ICAL.Component} component The component to add 2644 * @return {ICAL.Component} The passed in component 2645 */ 2646 addSubcomponent: function(component) { 2647 if (!this._components) { 2648 this._components = []; 2649 this._hydratedComponentCount = 0; 2650 } 2651 2652 if (component.parent) { 2653 component.parent.removeSubcomponent(component); 2654 } 2655 2656 var idx = this.jCal[COMPONENT_INDEX].push(component.jCal); 2657 this._components[idx - 1] = component; 2658 this._hydratedComponentCount++; 2659 component.parent = this; 2660 return component; 2661 }, 2662 2663 /** 2664 * Removes a single component by name or the instance of a specific 2665 * component. 2666 * 2667 * @param {ICAL.Component|String} nameOrComp Name of component, or component 2668 * @return {Boolean} True when comp is removed 2669 */ 2670 removeSubcomponent: function(nameOrComp) { 2671 var removed = this._removeObject(COMPONENT_INDEX, '_components', nameOrComp); 2672 if (removed) { 2673 this._hydratedComponentCount--; 2674 } 2675 return removed; 2676 }, 2677 2678 /** 2679 * Removes all components or (if given) all components by a particular 2680 * name. 2681 * 2682 * @param {String=} name Lowercase component name 2683 */ 2684 removeAllSubcomponents: function(name) { 2685 var removed = this._removeAllObjects(COMPONENT_INDEX, '_components', name); 2686 this._hydratedComponentCount = 0; 2687 return removed; 2688 }, 2689 2690 /** 2691 * Adds an {@link ICAL.Property} to the component. 2692 * 2693 * @param {ICAL.Property} property The property to add 2694 * @return {ICAL.Property} The passed in property 2695 */ 2696 addProperty: function(property) { 2697 if (!(property instanceof ICAL.Property)) { 2698 throw new TypeError('must instance of ICAL.Property'); 2699 } 2700 2701 if (!this._properties) { 2702 this._properties = []; 2703 this._hydratedPropertyCount = 0; 2704 } 2705 2706 if (property.parent) { 2707 property.parent.removeProperty(property); 2708 } 2709 2710 var idx = this.jCal[PROPERTY_INDEX].push(property.jCal); 2711 this._properties[idx - 1] = property; 2712 this._hydratedPropertyCount++; 2713 property.parent = this; 2714 return property; 2715 }, 2716 2717 /** 2718 * Helper method to add a property with a value to the component. 2719 * 2720 * @param {String} name Property name to add 2721 * @param {String|Number|Object} value Property value 2722 * @return {ICAL.Property} The created property 2723 */ 2724 addPropertyWithValue: function(name, value) { 2725 var prop = new ICAL.Property(name); 2726 prop.setValue(value); 2727 2728 this.addProperty(prop); 2729 2730 return prop; 2731 }, 2732 2733 /** 2734 * Helper method that will update or create a property of the given name 2735 * and sets its value. If multiple properties with the given name exist, 2736 * only the first is updated. 2737 * 2738 * @param {String} name Property name to update 2739 * @param {String|Number|Object} value Property value 2740 * @return {ICAL.Property} The created property 2741 */ 2742 updatePropertyWithValue: function(name, value) { 2743 var prop = this.getFirstProperty(name); 2744 2745 if (prop) { 2746 prop.setValue(value); 2747 } else { 2748 prop = this.addPropertyWithValue(name, value); 2749 } 2750 2751 return prop; 2752 }, 2753 2754 /** 2755 * Removes a single property by name or the instance of the specific 2756 * property. 2757 * 2758 * @param {String|ICAL.Property} nameOrProp Property name or instance to remove 2759 * @return {Boolean} True, when deleted 2760 */ 2761 removeProperty: function(nameOrProp) { 2762 var removed = this._removeObject(PROPERTY_INDEX, '_properties', nameOrProp); 2763 if (removed) { 2764 this._hydratedPropertyCount--; 2765 } 2766 return removed; 2767 }, 2768 2769 /** 2770 * Removes all properties associated with this component, optionally 2771 * filtered by name. 2772 * 2773 * @param {String=} name Lowercase property name 2774 * @return {Boolean} True, when deleted 2775 */ 2776 removeAllProperties: function(name) { 2777 var removed = this._removeAllObjects(PROPERTY_INDEX, '_properties', name); 2778 this._hydratedPropertyCount = 0; 2779 return removed; 2780 }, 2781 2782 /** 2783 * Returns the Object representation of this component. The returned object 2784 * is a live jCal object and should be cloned if modified. 2785 * @return {Object} 2786 */ 2787 toJSON: function() { 2788 return this.jCal; 2789 }, 2790 2791 /** 2792 * The string representation of this component. 2793 * @return {String} 2794 */ 2795 toString: function() { 2796 return ICAL.stringify.component( 2797 this.jCal, this._designSet 2798 ); 2799 } 2800 }; 2801 2802 /** 2803 * Create an {@link ICAL.Component} by parsing the passed iCalendar string. 2804 * 2805 * @param {String} str The iCalendar string to parse 2806 */ 2807 Component.fromString = function(str) { 2808 return new Component(ICAL.parse.component(str)); 2809 }; 2810 2811 return Component; 2812}()); 2813/* This Source Code Form is subject to the terms of the Mozilla Public 2814 * License, v. 2.0. If a copy of the MPL was not distributed with this 2815 * file, You can obtain one at http://mozilla.org/MPL/2.0/. 2816 * Portions Copyright (C) Philipp Kewisch, 2011-2015 */ 2817 2818 2819/** 2820 * This symbol is further described later on 2821 * @ignore 2822 */ 2823ICAL.Property = (function() { 2824 'use strict'; 2825 2826 var NAME_INDEX = 0; 2827 var PROP_INDEX = 1; 2828 var TYPE_INDEX = 2; 2829 var VALUE_INDEX = 3; 2830 2831 var design = ICAL.design; 2832 2833 /** 2834 * @classdesc 2835 * Provides a layer on top of the raw jCal object for manipulating a single 2836 * property, with its parameters and value. 2837 * 2838 * @description 2839 * Its important to note that mutations done in the wrapper 2840 * directly mutate the jCal object used to initialize. 2841 * 2842 * Can also be used to create new properties by passing 2843 * the name of the property (as a String). 2844 * 2845 * @class 2846 * @alias ICAL.Property 2847 * @param {Array|String} jCal Raw jCal representation OR 2848 * the new name of the property 2849 * 2850 * @param {ICAL.Component=} parent Parent component 2851 */ 2852 function Property(jCal, parent) { 2853 this._parent = parent || null; 2854 2855 if (typeof(jCal) === 'string') { 2856 // We are creating the property by name and need to detect the type 2857 this.jCal = [jCal, {}, design.defaultType]; 2858 this.jCal[TYPE_INDEX] = this.getDefaultType(); 2859 } else { 2860 this.jCal = jCal; 2861 } 2862 this._updateType(); 2863 } 2864 2865 Property.prototype = { 2866 2867 /** 2868 * The value type for this property 2869 * @readonly 2870 * @type {String} 2871 */ 2872 get type() { 2873 return this.jCal[TYPE_INDEX]; 2874 }, 2875 2876 /** 2877 * The name of this property, in lowercase. 2878 * @readonly 2879 * @type {String} 2880 */ 2881 get name() { 2882 return this.jCal[NAME_INDEX]; 2883 }, 2884 2885 /** 2886 * The parent component for this property. 2887 * @type {ICAL.Component} 2888 */ 2889 get parent() { 2890 return this._parent; 2891 }, 2892 2893 set parent(p) { 2894 // Before setting the parent, check if the design set has changed. If it 2895 // has, we later need to update the type if it was unknown before. 2896 var designSetChanged = !this._parent || (p && p._designSet != this._parent._designSet); 2897 2898 this._parent = p; 2899 2900 if (this.type == design.defaultType && designSetChanged) { 2901 this.jCal[TYPE_INDEX] = this.getDefaultType(); 2902 this._updateType(); 2903 } 2904 2905 return p; 2906 }, 2907 2908 /** 2909 * The design set for this property, e.g. icalendar vs vcard 2910 * 2911 * @type {ICAL.design.designSet} 2912 * @private 2913 */ 2914 get _designSet() { 2915 return this.parent ? this.parent._designSet : design.defaultSet; 2916 }, 2917 2918 /** 2919 * Updates the type metadata from the current jCal type and design set. 2920 * 2921 * @private 2922 */ 2923 _updateType: function() { 2924 var designSet = this._designSet; 2925 2926 if (this.type in designSet.value) { 2927 var designType = designSet.value[this.type]; 2928 2929 if ('decorate' in designSet.value[this.type]) { 2930 this.isDecorated = true; 2931 } else { 2932 this.isDecorated = false; 2933 } 2934 2935 if (this.name in designSet.property) { 2936 this.isMultiValue = ('multiValue' in designSet.property[this.name]); 2937 this.isStructuredValue = ('structuredValue' in designSet.property[this.name]); 2938 } 2939 } 2940 }, 2941 2942 /** 2943 * Hydrate a single value. The act of hydrating means turning the raw jCal 2944 * value into a potentially wrapped object, for example {@link ICAL.Time}. 2945 * 2946 * @private 2947 * @param {Number} index The index of the value to hydrate 2948 * @return {Object} The decorated value. 2949 */ 2950 _hydrateValue: function(index) { 2951 if (this._values && this._values[index]) { 2952 return this._values[index]; 2953 } 2954 2955 // for the case where there is no value. 2956 if (this.jCal.length <= (VALUE_INDEX + index)) { 2957 return null; 2958 } 2959 2960 if (this.isDecorated) { 2961 if (!this._values) { 2962 this._values = []; 2963 } 2964 return (this._values[index] = this._decorate( 2965 this.jCal[VALUE_INDEX + index] 2966 )); 2967 } else { 2968 return this.jCal[VALUE_INDEX + index]; 2969 } 2970 }, 2971 2972 /** 2973 * Decorate a single value, returning its wrapped object. This is used by 2974 * the hydrate function to actually wrap the value. 2975 * 2976 * @private 2977 * @param {?} value The value to decorate 2978 * @return {Object} The decorated value 2979 */ 2980 _decorate: function(value) { 2981 return this._designSet.value[this.type].decorate(value, this); 2982 }, 2983 2984 /** 2985 * Undecorate a single value, returning its raw jCal data. 2986 * 2987 * @private 2988 * @param {Object} value The value to undecorate 2989 * @return {?} The undecorated value 2990 */ 2991 _undecorate: function(value) { 2992 return this._designSet.value[this.type].undecorate(value, this); 2993 }, 2994 2995 /** 2996 * Sets the value at the given index while also hydrating it. The passed 2997 * value can either be a decorated or undecorated value. 2998 * 2999 * @private 3000 * @param {?} value The value to set 3001 * @param {Number} index The index to set it at 3002 */ 3003 _setDecoratedValue: function(value, index) { 3004 if (!this._values) { 3005 this._values = []; 3006 } 3007 3008 if (typeof(value) === 'object' && 'icaltype' in value) { 3009 // decorated value 3010 this.jCal[VALUE_INDEX + index] = this._undecorate(value); 3011 this._values[index] = value; 3012 } else { 3013 // undecorated value 3014 this.jCal[VALUE_INDEX + index] = value; 3015 this._values[index] = this._decorate(value); 3016 } 3017 }, 3018 3019 /** 3020 * Gets a parameter on the property. 3021 * 3022 * @param {String} name Property name (lowercase) 3023 * @return {Array|String} Property value 3024 */ 3025 getParameter: function(name) { 3026 if (name in this.jCal[PROP_INDEX]) { 3027 return this.jCal[PROP_INDEX][name]; 3028 } else { 3029 return undefined; 3030 } 3031 }, 3032 3033 /** 3034 * Gets first parameter on the property. 3035 * 3036 * @param {String} name Property name (lowercase) 3037 * @return {String} Property value 3038 */ 3039 getFirstParameter: function(name) { 3040 var parameters = this.getParameter(name); 3041 3042 if (Array.isArray(parameters)) { 3043 return parameters[0]; 3044 } 3045 3046 return parameters; 3047 }, 3048 3049 /** 3050 * Sets a parameter on the property. 3051 * 3052 * @param {String} name The parameter name 3053 * @param {Array|String} value The parameter value 3054 */ 3055 setParameter: function(name, value) { 3056 var lcname = name.toLowerCase(); 3057 if (typeof value === "string" && 3058 lcname in this._designSet.param && 3059 'multiValue' in this._designSet.param[lcname]) { 3060 value = [value]; 3061 } 3062 this.jCal[PROP_INDEX][name] = value; 3063 }, 3064 3065 /** 3066 * Removes a parameter 3067 * 3068 * @param {String} name The parameter name 3069 */ 3070 removeParameter: function(name) { 3071 delete this.jCal[PROP_INDEX][name]; 3072 }, 3073 3074 /** 3075 * Get the default type based on this property's name. 3076 * 3077 * @return {String} The default type for this property 3078 */ 3079 getDefaultType: function() { 3080 var name = this.jCal[NAME_INDEX]; 3081 var designSet = this._designSet; 3082 3083 if (name in designSet.property) { 3084 var details = designSet.property[name]; 3085 if ('defaultType' in details) { 3086 return details.defaultType; 3087 } 3088 } 3089 return design.defaultType; 3090 }, 3091 3092 /** 3093 * Sets type of property and clears out any existing values of the current 3094 * type. 3095 * 3096 * @param {String} type New iCAL type (see design.*.values) 3097 */ 3098 resetType: function(type) { 3099 this.removeAllValues(); 3100 this.jCal[TYPE_INDEX] = type; 3101 this._updateType(); 3102 }, 3103 3104 /** 3105 * Finds the first property value. 3106 * 3107 * @return {String} First property value 3108 */ 3109 getFirstValue: function() { 3110 return this._hydrateValue(0); 3111 }, 3112 3113 /** 3114 * Gets all values on the property. 3115 * 3116 * NOTE: this creates an array during each call. 3117 * 3118 * @return {Array} List of values 3119 */ 3120 getValues: function() { 3121 var len = this.jCal.length - VALUE_INDEX; 3122 3123 if (len < 1) { 3124 // its possible for a property to have no value. 3125 return []; 3126 } 3127 3128 var i = 0; 3129 var result = []; 3130 3131 for (; i < len; i++) { 3132 result[i] = this._hydrateValue(i); 3133 } 3134 3135 return result; 3136 }, 3137 3138 /** 3139 * Removes all values from this property 3140 */ 3141 removeAllValues: function() { 3142 if (this._values) { 3143 this._values.length = 0; 3144 } 3145 this.jCal.length = 3; 3146 }, 3147 3148 /** 3149 * Sets the values of the property. Will overwrite the existing values. 3150 * This can only be used for multi-value properties. 3151 * 3152 * @param {Array} values An array of values 3153 */ 3154 setValues: function(values) { 3155 if (!this.isMultiValue) { 3156 throw new Error( 3157 this.name + ': does not not support mulitValue.\n' + 3158 'override isMultiValue' 3159 ); 3160 } 3161 3162 var len = values.length; 3163 var i = 0; 3164 this.removeAllValues(); 3165 3166 if (len > 0 && 3167 typeof(values[0]) === 'object' && 3168 'icaltype' in values[0]) { 3169 this.resetType(values[0].icaltype); 3170 } 3171 3172 if (this.isDecorated) { 3173 for (; i < len; i++) { 3174 this._setDecoratedValue(values[i], i); 3175 } 3176 } else { 3177 for (; i < len; i++) { 3178 this.jCal[VALUE_INDEX + i] = values[i]; 3179 } 3180 } 3181 }, 3182 3183 /** 3184 * Sets the current value of the property. If this is a multi-value 3185 * property, all other values will be removed. 3186 * 3187 * @param {String|Object} value New property value. 3188 */ 3189 setValue: function(value) { 3190 this.removeAllValues(); 3191 if (typeof(value) === 'object' && 'icaltype' in value) { 3192 this.resetType(value.icaltype); 3193 } 3194 3195 if (this.isDecorated) { 3196 this._setDecoratedValue(value, 0); 3197 } else { 3198 this.jCal[VALUE_INDEX] = value; 3199 } 3200 }, 3201 3202 /** 3203 * Returns the Object representation of this component. The returned object 3204 * is a live jCal object and should be cloned if modified. 3205 * @return {Object} 3206 */ 3207 toJSON: function() { 3208 return this.jCal; 3209 }, 3210 3211 /** 3212 * The string representation of this component. 3213 * @return {String} 3214 */ 3215 toICALString: function() { 3216 return ICAL.stringify.property( 3217 this.jCal, this._designSet, true 3218 ); 3219 } 3220 }; 3221 3222 /** 3223 * Create an {@link ICAL.Property} by parsing the passed iCalendar string. 3224 * 3225 * @param {String} str The iCalendar string to parse 3226 * @param {ICAL.design.designSet=} designSet The design data to use for this property 3227 * @return {ICAL.Property} The created iCalendar property 3228 */ 3229 Property.fromString = function(str, designSet) { 3230 return new Property(ICAL.parse.property(str, designSet)); 3231 }; 3232 3233 return Property; 3234}()); 3235/* This Source Code Form is subject to the terms of the Mozilla Public 3236 * License, v. 2.0. If a copy of the MPL was not distributed with this 3237 * file, You can obtain one at http://mozilla.org/MPL/2.0/. 3238 * Portions Copyright (C) Philipp Kewisch, 2011-2015 */ 3239 3240 3241/** 3242 * This symbol is further described later on 3243 * @ignore 3244 */ 3245ICAL.UtcOffset = (function() { 3246 3247 /** 3248 * @classdesc 3249 * This class represents the "duration" value type, with various calculation 3250 * and manipulation methods. 3251 * 3252 * @class 3253 * @alias ICAL.UtcOffset 3254 * @param {Object} aData An object with members of the utc offset 3255 * @param {Number=} aData.hours The hours for the utc offset 3256 * @param {Number=} aData.minutes The minutes in the utc offset 3257 * @param {Number=} aData.factor The factor for the utc-offset, either -1 or 1 3258 */ 3259 function UtcOffset(aData) { 3260 this.fromData(aData); 3261 } 3262 3263 UtcOffset.prototype = { 3264 3265 /** 3266 * The hours in the utc-offset 3267 * @type {Number} 3268 */ 3269 hours: 0, 3270 3271 /** 3272 * The minutes in the utc-offset 3273 * @type {Number} 3274 */ 3275 minutes: 0, 3276 3277 /** 3278 * The sign of the utc offset, 1 for positive offset, -1 for negative 3279 * offsets. 3280 * @type {Number} 3281 */ 3282 factor: 1, 3283 3284 /** 3285 * The type name, to be used in the jCal object. 3286 * @constant 3287 * @type {String} 3288 * @default "utc-offset" 3289 */ 3290 icaltype: "utc-offset", 3291 3292 /** 3293 * Returns a clone of the utc offset object. 3294 * 3295 * @return {ICAL.UtcOffset} The cloned object 3296 */ 3297 clone: function() { 3298 return ICAL.UtcOffset.fromSeconds(this.toSeconds()); 3299 }, 3300 3301 /** 3302 * Sets up the current instance using members from the passed data object. 3303 * 3304 * @param {Object} aData An object with members of the utc offset 3305 * @param {Number=} aData.hours The hours for the utc offset 3306 * @param {Number=} aData.minutes The minutes in the utc offset 3307 * @param {Number=} aData.factor The factor for the utc-offset, either -1 or 1 3308 */ 3309 fromData: function(aData) { 3310 if (aData) { 3311 for (var key in aData) { 3312 /* istanbul ignore else */ 3313 if (aData.hasOwnProperty(key)) { 3314 this[key] = aData[key]; 3315 } 3316 } 3317 } 3318 this._normalize(); 3319 }, 3320 3321 /** 3322 * Sets up the current instance from the given seconds value. The seconds 3323 * value is truncated to the minute. Offsets are wrapped when the world 3324 * ends, the hour after UTC+14:00 is UTC-12:00. 3325 * 3326 * @param {Number} aSeconds The seconds to convert into an offset 3327 */ 3328 fromSeconds: function(aSeconds) { 3329 var secs = Math.abs(aSeconds); 3330 3331 this.factor = aSeconds < 0 ? -1 : 1; 3332 this.hours = ICAL.helpers.trunc(secs / 3600); 3333 3334 secs -= (this.hours * 3600); 3335 this.minutes = ICAL.helpers.trunc(secs / 60); 3336 return this; 3337 }, 3338 3339 /** 3340 * Convert the current offset to a value in seconds 3341 * 3342 * @return {Number} The offset in seconds 3343 */ 3344 toSeconds: function() { 3345 return this.factor * (60 * this.minutes + 3600 * this.hours); 3346 }, 3347 3348 /** 3349 * Compare this utc offset with another one. 3350 * 3351 * @param {ICAL.UtcOffset} other The other offset to compare with 3352 * @return {Number} -1, 0 or 1 for less/equal/greater 3353 */ 3354 compare: function icaltime_compare(other) { 3355 var a = this.toSeconds(); 3356 var b = other.toSeconds(); 3357 return (a > b) - (b > a); 3358 }, 3359 3360 _normalize: function() { 3361 // Range: 97200 seconds (with 1 hour inbetween) 3362 var secs = this.toSeconds(); 3363 var factor = this.factor; 3364 while (secs < -43200) { // = UTC-12:00 3365 secs += 97200; 3366 } 3367 while (secs > 50400) { // = UTC+14:00 3368 secs -= 97200; 3369 } 3370 3371 this.fromSeconds(secs); 3372 3373 // Avoid changing the factor when on zero seconds 3374 if (secs == 0) { 3375 this.factor = factor; 3376 } 3377 }, 3378 3379 /** 3380 * The iCalendar string representation of this utc-offset. 3381 * @return {String} 3382 */ 3383 toICALString: function() { 3384 return ICAL.design.icalendar.value['utc-offset'].toICAL(this.toString()); 3385 }, 3386 3387 /** 3388 * The string representation of this utc-offset. 3389 * @return {String} 3390 */ 3391 toString: function toString() { 3392 return (this.factor == 1 ? "+" : "-") + 3393 ICAL.helpers.pad2(this.hours) + ':' + 3394 ICAL.helpers.pad2(this.minutes); 3395 } 3396 }; 3397 3398 /** 3399 * Creates a new {@link ICAL.UtcOffset} instance from the passed string. 3400 * 3401 * @param {String} aString The string to parse 3402 * @return {ICAL.Duration} The created utc-offset instance 3403 */ 3404 UtcOffset.fromString = function(aString) { 3405 // -05:00 3406 var options = {}; 3407 //TODO: support seconds per rfc5545 ? 3408 options.factor = (aString[0] === '+') ? 1 : -1; 3409 options.hours = ICAL.helpers.strictParseInt(aString.substr(1, 2)); 3410 options.minutes = ICAL.helpers.strictParseInt(aString.substr(4, 2)); 3411 3412 return new ICAL.UtcOffset(options); 3413 }; 3414 3415 /** 3416 * Creates a new {@link ICAL.UtcOffset} instance from the passed seconds 3417 * value. 3418 * 3419 * @param {Number} aSeconds The number of seconds to convert 3420 */ 3421 UtcOffset.fromSeconds = function(aSeconds) { 3422 var instance = new UtcOffset(); 3423 instance.fromSeconds(aSeconds); 3424 return instance; 3425 }; 3426 3427 return UtcOffset; 3428}()); 3429/* This Source Code Form is subject to the terms of the Mozilla Public 3430 * License, v. 2.0. If a copy of the MPL was not distributed with this 3431 * file, You can obtain one at http://mozilla.org/MPL/2.0/. 3432 * Portions Copyright (C) Philipp Kewisch, 2011-2015 */ 3433 3434 3435/** 3436 * This symbol is further described later on 3437 * @ignore 3438 */ 3439ICAL.Binary = (function() { 3440 3441 /** 3442 * @classdesc 3443 * Represents the BINARY value type, which contains extra methods for 3444 * encoding and decoding. 3445 * 3446 * @class 3447 * @alias ICAL.Binary 3448 * @param {String} aValue The binary data for this value 3449 */ 3450 function Binary(aValue) { 3451 this.value = aValue; 3452 } 3453 3454 Binary.prototype = { 3455 /** 3456 * The type name, to be used in the jCal object. 3457 * @default "binary" 3458 * @constant 3459 */ 3460 icaltype: "binary", 3461 3462 /** 3463 * Base64 decode the current value 3464 * 3465 * @return {String} The base64-decoded value 3466 */ 3467 decodeValue: function decodeValue() { 3468 return this._b64_decode(this.value); 3469 }, 3470 3471 /** 3472 * Encodes the passed parameter with base64 and sets the internal 3473 * value to the result. 3474 * 3475 * @param {String} aValue The raw binary value to encode 3476 */ 3477 setEncodedValue: function setEncodedValue(aValue) { 3478 this.value = this._b64_encode(aValue); 3479 }, 3480 3481 _b64_encode: function base64_encode(data) { 3482 // http://kevin.vanzonneveld.net 3483 // + original by: Tyler Akins (http://rumkin.com) 3484 // + improved by: Bayron Guevara 3485 // + improved by: Thunder.m 3486 // + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) 3487 // + bugfixed by: Pellentesque Malesuada 3488 // + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) 3489 // + improved by: Rafał Kukawski (http://kukawski.pl) 3490 // * example 1: base64_encode('Kevin van Zonneveld'); 3491 // * returns 1: 'S2V2aW4gdmFuIFpvbm5ldmVsZA==' 3492 // mozilla has this native 3493 // - but breaks in 2.0.0.12! 3494 //if (typeof this.window['atob'] == 'function') { 3495 // return atob(data); 3496 //} 3497 var b64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + 3498 "abcdefghijklmnopqrstuvwxyz0123456789+/="; 3499 var o1, o2, o3, h1, h2, h3, h4, bits, i = 0, 3500 ac = 0, 3501 enc = "", 3502 tmp_arr = []; 3503 3504 if (!data) { 3505 return data; 3506 } 3507 3508 do { // pack three octets into four hexets 3509 o1 = data.charCodeAt(i++); 3510 o2 = data.charCodeAt(i++); 3511 o3 = data.charCodeAt(i++); 3512 3513 bits = o1 << 16 | o2 << 8 | o3; 3514 3515 h1 = bits >> 18 & 0x3f; 3516 h2 = bits >> 12 & 0x3f; 3517 h3 = bits >> 6 & 0x3f; 3518 h4 = bits & 0x3f; 3519 3520 // use hexets to index into b64, and append result to encoded string 3521 tmp_arr[ac++] = b64.charAt(h1) + b64.charAt(h2) + b64.charAt(h3) + b64.charAt(h4); 3522 } while (i < data.length); 3523 3524 enc = tmp_arr.join(''); 3525 3526 var r = data.length % 3; 3527 3528 return (r ? enc.slice(0, r - 3) : enc) + '==='.slice(r || 3); 3529 3530 }, 3531 3532 _b64_decode: function base64_decode(data) { 3533 // http://kevin.vanzonneveld.net 3534 // + original by: Tyler Akins (http://rumkin.com) 3535 // + improved by: Thunder.m 3536 // + input by: Aman Gupta 3537 // + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) 3538 // + bugfixed by: Onno Marsman 3539 // + bugfixed by: Pellentesque Malesuada 3540 // + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) 3541 // + input by: Brett Zamir (http://brett-zamir.me) 3542 // + bugfixed by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) 3543 // * example 1: base64_decode('S2V2aW4gdmFuIFpvbm5ldmVsZA=='); 3544 // * returns 1: 'Kevin van Zonneveld' 3545 // mozilla has this native 3546 // - but breaks in 2.0.0.12! 3547 //if (typeof this.window['btoa'] == 'function') { 3548 // return btoa(data); 3549 //} 3550 var b64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + 3551 "abcdefghijklmnopqrstuvwxyz0123456789+/="; 3552 var o1, o2, o3, h1, h2, h3, h4, bits, i = 0, 3553 ac = 0, 3554 dec = "", 3555 tmp_arr = []; 3556 3557 if (!data) { 3558 return data; 3559 } 3560 3561 data += ''; 3562 3563 do { // unpack four hexets into three octets using index points in b64 3564 h1 = b64.indexOf(data.charAt(i++)); 3565 h2 = b64.indexOf(data.charAt(i++)); 3566 h3 = b64.indexOf(data.charAt(i++)); 3567 h4 = b64.indexOf(data.charAt(i++)); 3568 3569 bits = h1 << 18 | h2 << 12 | h3 << 6 | h4; 3570 3571 o1 = bits >> 16 & 0xff; 3572 o2 = bits >> 8 & 0xff; 3573 o3 = bits & 0xff; 3574 3575 if (h3 == 64) { 3576 tmp_arr[ac++] = String.fromCharCode(o1); 3577 } else if (h4 == 64) { 3578 tmp_arr[ac++] = String.fromCharCode(o1, o2); 3579 } else { 3580 tmp_arr[ac++] = String.fromCharCode(o1, o2, o3); 3581 } 3582 } while (i < data.length); 3583 3584 dec = tmp_arr.join(''); 3585 3586 return dec; 3587 }, 3588 3589 /** 3590 * The string representation of this value 3591 * @return {String} 3592 */ 3593 toString: function() { 3594 return this.value; 3595 } 3596 }; 3597 3598 /** 3599 * Creates a binary value from the given string. 3600 * 3601 * @param {String} aString The binary value string 3602 * @return {ICAL.Binary} The binary value instance 3603 */ 3604 Binary.fromString = function(aString) { 3605 return new Binary(aString); 3606 }; 3607 3608 return Binary; 3609}()); 3610/* This Source Code Form is subject to the terms of the Mozilla Public 3611 * License, v. 2.0. If a copy of the MPL was not distributed with this 3612 * file, You can obtain one at http://mozilla.org/MPL/2.0/. 3613 * Portions Copyright (C) Philipp Kewisch, 2011-2015 */ 3614 3615 3616 3617(function() { 3618 /** 3619 * @classdesc 3620 * This class represents the "period" value type, with various calculation 3621 * and manipulation methods. 3622 * 3623 * @description 3624 * The passed data object cannot contain both and end date and a duration. 3625 * 3626 * @class 3627 * @param {Object} aData An object with members of the period 3628 * @param {ICAL.Time=} aData.start The start of the period 3629 * @param {ICAL.Time=} aData.end The end of the period 3630 * @param {ICAL.Duration=} aData.duration The duration of the period 3631 */ 3632 ICAL.Period = function icalperiod(aData) { 3633 this.wrappedJSObject = this; 3634 3635 if (aData && 'start' in aData) { 3636 if (aData.start && !(aData.start instanceof ICAL.Time)) { 3637 throw new TypeError('.start must be an instance of ICAL.Time'); 3638 } 3639 this.start = aData.start; 3640 } 3641 3642 if (aData && aData.end && aData.duration) { 3643 throw new Error('cannot accept both end and duration'); 3644 } 3645 3646 if (aData && 'end' in aData) { 3647 if (aData.end && !(aData.end instanceof ICAL.Time)) { 3648 throw new TypeError('.end must be an instance of ICAL.Time'); 3649 } 3650 this.end = aData.end; 3651 } 3652 3653 if (aData && 'duration' in aData) { 3654 if (aData.duration && !(aData.duration instanceof ICAL.Duration)) { 3655 throw new TypeError('.duration must be an instance of ICAL.Duration'); 3656 } 3657 this.duration = aData.duration; 3658 } 3659 }; 3660 3661 ICAL.Period.prototype = { 3662 3663 /** 3664 * The start of the period 3665 * @type {ICAL.Time} 3666 */ 3667 start: null, 3668 3669 /** 3670 * The end of the period 3671 * @type {ICAL.Time} 3672 */ 3673 end: null, 3674 3675 /** 3676 * The duration of the period 3677 * @type {ICAL.Duration} 3678 */ 3679 duration: null, 3680 3681 /** 3682 * The class identifier. 3683 * @constant 3684 * @type {String} 3685 * @default "icalperiod" 3686 */ 3687 icalclass: "icalperiod", 3688 3689 /** 3690 * The type name, to be used in the jCal object. 3691 * @constant 3692 * @type {String} 3693 * @default "period" 3694 */ 3695 icaltype: "period", 3696 3697 /** 3698 * Returns a clone of the duration object. 3699 * 3700 * @return {ICAL.Period} The cloned object 3701 */ 3702 clone: function() { 3703 return ICAL.Period.fromData({ 3704 start: this.start ? this.start.clone() : null, 3705 end: this.end ? this.end.clone() : null, 3706 duration: this.duration ? this.duration.clone() : null 3707 }); 3708 }, 3709 3710 /** 3711 * Calculates the duration of the period, either directly or by subtracting 3712 * start from end date. 3713 * 3714 * @return {ICAL.Duration} The calculated duration 3715 */ 3716 getDuration: function duration() { 3717 if (this.duration) { 3718 return this.duration; 3719 } else { 3720 return this.end.subtractDate(this.start); 3721 } 3722 }, 3723 3724 /** 3725 * Calculates the end date of the period, either directly or by adding 3726 * duration to start date. 3727 * 3728 * @return {ICAL.Time} The calculated end date 3729 */ 3730 getEnd: function() { 3731 if (this.end) { 3732 return this.end; 3733 } else { 3734 var end = this.start.clone(); 3735 end.addDuration(this.duration); 3736 return end; 3737 } 3738 }, 3739 3740 /** 3741 * The string representation of this period. 3742 * @return {String} 3743 */ 3744 toString: function toString() { 3745 return this.start + "/" + (this.end || this.duration); 3746 }, 3747 3748 /** 3749 * The jCal representation of this period type. 3750 * @return {Object} 3751 */ 3752 toJSON: function() { 3753 return [this.start.toString(), (this.end || this.duration).toString()]; 3754 }, 3755 3756 /** 3757 * The iCalendar string representation of this period. 3758 * @return {String} 3759 */ 3760 toICALString: function() { 3761 return this.start.toICALString() + "/" + 3762 (this.end || this.duration).toICALString(); 3763 } 3764 }; 3765 3766 /** 3767 * Creates a new {@link ICAL.Period} instance from the passed string. 3768 * 3769 * @param {String} str The string to parse 3770 * @param {ICAL.Property} prop The property this period will be on 3771 * @return {ICAL.Period} The created period instance 3772 */ 3773 ICAL.Period.fromString = function fromString(str, prop) { 3774 var parts = str.split('/'); 3775 3776 if (parts.length !== 2) { 3777 throw new Error( 3778 'Invalid string value: "' + str + '" must contain a "/" char.' 3779 ); 3780 } 3781 3782 var options = { 3783 start: ICAL.Time.fromDateTimeString(parts[0], prop) 3784 }; 3785 3786 var end = parts[1]; 3787 3788 if (ICAL.Duration.isValueString(end)) { 3789 options.duration = ICAL.Duration.fromString(end); 3790 } else { 3791 options.end = ICAL.Time.fromDateTimeString(end, prop); 3792 } 3793 3794 return new ICAL.Period(options); 3795 }; 3796 3797 /** 3798 * Creates a new {@link ICAL.Period} instance from the given data object. 3799 * The passed data object cannot contain both and end date and a duration. 3800 * 3801 * @param {Object} aData An object with members of the period 3802 * @param {ICAL.Time=} aData.start The start of the period 3803 * @param {ICAL.Time=} aData.end The end of the period 3804 * @param {ICAL.Duration=} aData.duration The duration of the period 3805 * @return {ICAL.Period} The period instance 3806 */ 3807 ICAL.Period.fromData = function fromData(aData) { 3808 return new ICAL.Period(aData); 3809 }; 3810 3811 /** 3812 * Returns a new period instance from the given jCal data array. The first 3813 * member is always the start date string, the second member is either a 3814 * duration or end date string. 3815 * 3816 * @param {Array<String,String>} aData The jCal data array 3817 * @param {ICAL.Property} aProp The property this jCal data is on 3818 * @param {Boolean} aLenient If true, data value can be both date and date-time 3819 * @return {ICAL.Period} The period instance 3820 */ 3821 ICAL.Period.fromJSON = function(aData, aProp, aLenient) { 3822 function fromDateOrDateTimeString(aValue, aProp) { 3823 if (aLenient) { 3824 return ICAL.Time.fromString(aValue, aProp); 3825 } else { 3826 return ICAL.Time.fromDateTimeString(aValue, aProp); 3827 } 3828 } 3829 3830 if (ICAL.Duration.isValueString(aData[1])) { 3831 return ICAL.Period.fromData({ 3832 start: fromDateOrDateTimeString(aData[0], aProp), 3833 duration: ICAL.Duration.fromString(aData[1]) 3834 }); 3835 } else { 3836 return ICAL.Period.fromData({ 3837 start: fromDateOrDateTimeString(aData[0], aProp), 3838 end: fromDateOrDateTimeString(aData[1], aProp) 3839 }); 3840 } 3841 }; 3842})(); 3843/* This Source Code Form is subject to the terms of the Mozilla Public 3844 * License, v. 2.0. If a copy of the MPL was not distributed with this 3845 * file, You can obtain one at http://mozilla.org/MPL/2.0/. 3846 * Portions Copyright (C) Philipp Kewisch, 2011-2015 */ 3847 3848 3849 3850(function() { 3851 var DURATION_LETTERS = /([PDWHMTS]{1,1})/; 3852 3853 /** 3854 * @classdesc 3855 * This class represents the "duration" value type, with various calculation 3856 * and manipulation methods. 3857 * 3858 * @class 3859 * @alias ICAL.Duration 3860 * @param {Object} data An object with members of the duration 3861 * @param {Number} data.weeks Duration in weeks 3862 * @param {Number} data.days Duration in days 3863 * @param {Number} data.hours Duration in hours 3864 * @param {Number} data.minutes Duration in minutes 3865 * @param {Number} data.seconds Duration in seconds 3866 * @param {Boolean} data.isNegative If true, the duration is negative 3867 */ 3868 ICAL.Duration = function icalduration(data) { 3869 this.wrappedJSObject = this; 3870 this.fromData(data); 3871 }; 3872 3873 ICAL.Duration.prototype = { 3874 /** 3875 * The weeks in this duration 3876 * @type {Number} 3877 * @default 0 3878 */ 3879 weeks: 0, 3880 3881 /** 3882 * The days in this duration 3883 * @type {Number} 3884 * @default 0 3885 */ 3886 days: 0, 3887 3888 /** 3889 * The days in this duration 3890 * @type {Number} 3891 * @default 0 3892 */ 3893 hours: 0, 3894 3895 /** 3896 * The minutes in this duration 3897 * @type {Number} 3898 * @default 0 3899 */ 3900 minutes: 0, 3901 3902 /** 3903 * The seconds in this duration 3904 * @type {Number} 3905 * @default 0 3906 */ 3907 seconds: 0, 3908 3909 /** 3910 * The seconds in this duration 3911 * @type {Boolean} 3912 * @default false 3913 */ 3914 isNegative: false, 3915 3916 /** 3917 * The class identifier. 3918 * @constant 3919 * @type {String} 3920 * @default "icalduration" 3921 */ 3922 icalclass: "icalduration", 3923 3924 /** 3925 * The type name, to be used in the jCal object. 3926 * @constant 3927 * @type {String} 3928 * @default "duration" 3929 */ 3930 icaltype: "duration", 3931 3932 /** 3933 * Returns a clone of the duration object. 3934 * 3935 * @return {ICAL.Duration} The cloned object 3936 */ 3937 clone: function clone() { 3938 return ICAL.Duration.fromData(this); 3939 }, 3940 3941 /** 3942 * The duration value expressed as a number of seconds. 3943 * 3944 * @return {Number} The duration value in seconds 3945 */ 3946 toSeconds: function toSeconds() { 3947 var seconds = this.seconds + 60 * this.minutes + 3600 * this.hours + 3948 86400 * this.days + 7 * 86400 * this.weeks; 3949 return (this.isNegative ? -seconds : seconds); 3950 }, 3951 3952 /** 3953 * Reads the passed seconds value into this duration object. Afterwards, 3954 * members like {@link ICAL.Duration#days days} and {@link ICAL.Duration#weeks weeks} will be set up 3955 * accordingly. 3956 * 3957 * @param {Number} aSeconds The duration value in seconds 3958 * @return {ICAL.Duration} Returns this instance 3959 */ 3960 fromSeconds: function fromSeconds(aSeconds) { 3961 var secs = Math.abs(aSeconds); 3962 3963 this.isNegative = (aSeconds < 0); 3964 this.days = ICAL.helpers.trunc(secs / 86400); 3965 3966 // If we have a flat number of weeks, use them. 3967 if (this.days % 7 == 0) { 3968 this.weeks = this.days / 7; 3969 this.days = 0; 3970 } else { 3971 this.weeks = 0; 3972 } 3973 3974 secs -= (this.days + 7 * this.weeks) * 86400; 3975 3976 this.hours = ICAL.helpers.trunc(secs / 3600); 3977 secs -= this.hours * 3600; 3978 3979 this.minutes = ICAL.helpers.trunc(secs / 60); 3980 secs -= this.minutes * 60; 3981 3982 this.seconds = secs; 3983 return this; 3984 }, 3985 3986 /** 3987 * Sets up the current instance using members from the passed data object. 3988 * 3989 * @param {Object} aData An object with members of the duration 3990 * @param {Number} aData.weeks Duration in weeks 3991 * @param {Number} aData.days Duration in days 3992 * @param {Number} aData.hours Duration in hours 3993 * @param {Number} aData.minutes Duration in minutes 3994 * @param {Number} aData.seconds Duration in seconds 3995 * @param {Boolean} aData.isNegative If true, the duration is negative 3996 */ 3997 fromData: function fromData(aData) { 3998 var propsToCopy = ["weeks", "days", "hours", 3999 "minutes", "seconds", "isNegative"]; 4000 for (var key in propsToCopy) { 4001 /* istanbul ignore if */ 4002 if (!propsToCopy.hasOwnProperty(key)) { 4003 continue; 4004 } 4005 var prop = propsToCopy[key]; 4006 if (aData && prop in aData) { 4007 this[prop] = aData[prop]; 4008 } else { 4009 this[prop] = 0; 4010 } 4011 } 4012 }, 4013 4014 /** 4015 * Resets the duration instance to the default values, i.e. PT0S 4016 */ 4017 reset: function reset() { 4018 this.isNegative = false; 4019 this.weeks = 0; 4020 this.days = 0; 4021 this.hours = 0; 4022 this.minutes = 0; 4023 this.seconds = 0; 4024 }, 4025 4026 /** 4027 * Compares the duration instance with another one. 4028 * 4029 * @param {ICAL.Duration} aOther The instance to compare with 4030 * @return {Number} -1, 0 or 1 for less/equal/greater 4031 */ 4032 compare: function compare(aOther) { 4033 var thisSeconds = this.toSeconds(); 4034 var otherSeconds = aOther.toSeconds(); 4035 return (thisSeconds > otherSeconds) - (thisSeconds < otherSeconds); 4036 }, 4037 4038 /** 4039 * Normalizes the duration instance. For example, a duration with a value 4040 * of 61 seconds will be normalized to 1 minute and 1 second. 4041 */ 4042 normalize: function normalize() { 4043 this.fromSeconds(this.toSeconds()); 4044 }, 4045 4046 /** 4047 * The string representation of this duration. 4048 * @return {String} 4049 */ 4050 toString: function toString() { 4051 if (this.toSeconds() == 0) { 4052 return "PT0S"; 4053 } else { 4054 var str = ""; 4055 if (this.isNegative) str += "-"; 4056 str += "P"; 4057 if (this.weeks) str += this.weeks + "W"; 4058 if (this.days) str += this.days + "D"; 4059 4060 if (this.hours || this.minutes || this.seconds) { 4061 str += "T"; 4062 if (this.hours) str += this.hours + "H"; 4063 if (this.minutes) str += this.minutes + "M"; 4064 if (this.seconds) str += this.seconds + "S"; 4065 } 4066 return str; 4067 } 4068 }, 4069 4070 /** 4071 * The iCalendar string representation of this duration. 4072 * @return {String} 4073 */ 4074 toICALString: function() { 4075 return this.toString(); 4076 } 4077 }; 4078 4079 /** 4080 * Returns a new ICAL.Duration instance from the passed seconds value. 4081 * 4082 * @param {Number} aSeconds The seconds to create the instance from 4083 * @return {ICAL.Duration} The newly created duration instance 4084 */ 4085 ICAL.Duration.fromSeconds = function icalduration_from_seconds(aSeconds) { 4086 return (new ICAL.Duration()).fromSeconds(aSeconds); 4087 }; 4088 4089 /** 4090 * Internal helper function to handle a chunk of a duration. 4091 * 4092 * @param {String} letter type of duration chunk 4093 * @param {String} number numeric value or -/+ 4094 * @param {Object} dict target to assign values to 4095 */ 4096 function parseDurationChunk(letter, number, object) { 4097 var type; 4098 switch (letter) { 4099 case 'P': 4100 if (number && number === '-') { 4101 object.isNegative = true; 4102 } else { 4103 object.isNegative = false; 4104 } 4105 // period 4106 break; 4107 case 'D': 4108 type = 'days'; 4109 break; 4110 case 'W': 4111 type = 'weeks'; 4112 break; 4113 case 'H': 4114 type = 'hours'; 4115 break; 4116 case 'M': 4117 type = 'minutes'; 4118 break; 4119 case 'S': 4120 type = 'seconds'; 4121 break; 4122 default: 4123 // Not a valid chunk 4124 return 0; 4125 } 4126 4127 if (type) { 4128 if (!number && number !== 0) { 4129 throw new Error( 4130 'invalid duration value: Missing number before "' + letter + '"' 4131 ); 4132 } 4133 var num = parseInt(number, 10); 4134 if (ICAL.helpers.isStrictlyNaN(num)) { 4135 throw new Error( 4136 'invalid duration value: Invalid number "' + number + '" before "' + letter + '"' 4137 ); 4138 } 4139 object[type] = num; 4140 } 4141 4142 return 1; 4143 } 4144 4145 /** 4146 * Checks if the given string is an iCalendar duration value. 4147 * 4148 * @param {String} value The raw ical value 4149 * @return {Boolean} True, if the given value is of the 4150 * duration ical type 4151 */ 4152 ICAL.Duration.isValueString = function(string) { 4153 return (string[0] === 'P' || string[1] === 'P'); 4154 }; 4155 4156 /** 4157 * Creates a new {@link ICAL.Duration} instance from the passed string. 4158 * 4159 * @param {String} aStr The string to parse 4160 * @return {ICAL.Duration} The created duration instance 4161 */ 4162 ICAL.Duration.fromString = function icalduration_from_string(aStr) { 4163 var pos = 0; 4164 var dict = Object.create(null); 4165 var chunks = 0; 4166 4167 while ((pos = aStr.search(DURATION_LETTERS)) !== -1) { 4168 var type = aStr[pos]; 4169 var numeric = aStr.substr(0, pos); 4170 aStr = aStr.substr(pos + 1); 4171 4172 chunks += parseDurationChunk(type, numeric, dict); 4173 } 4174 4175 if (chunks < 2) { 4176 // There must be at least a chunk with "P" and some unit chunk 4177 throw new Error( 4178 'invalid duration value: Not enough duration components in "' + aStr + '"' 4179 ); 4180 } 4181 4182 return new ICAL.Duration(dict); 4183 }; 4184 4185 /** 4186 * Creates a new ICAL.Duration instance from the given data object. 4187 * 4188 * @param {Object} aData An object with members of the duration 4189 * @param {Number} aData.weeks Duration in weeks 4190 * @param {Number} aData.days Duration in days 4191 * @param {Number} aData.hours Duration in hours 4192 * @param {Number} aData.minutes Duration in minutes 4193 * @param {Number} aData.seconds Duration in seconds 4194 * @param {Boolean} aData.isNegative If true, the duration is negative 4195 * @return {ICAL.Duration} The createad duration instance 4196 */ 4197 ICAL.Duration.fromData = function icalduration_from_data(aData) { 4198 return new ICAL.Duration(aData); 4199 }; 4200})(); 4201/* This Source Code Form is subject to the terms of the Mozilla Public 4202 * License, v. 2.0. If a copy of the MPL was not distributed with this 4203 * file, You can obtain one at http://mozilla.org/MPL/2.0/. 4204 * Portions Copyright (C) Philipp Kewisch, 2011-2012 */ 4205 4206 4207 4208(function() { 4209 var OPTIONS = ["tzid", "location", "tznames", 4210 "latitude", "longitude"]; 4211 4212 /** 4213 * @classdesc 4214 * Timezone representation, created by passing in a tzid and component. 4215 * 4216 * @example 4217 * var vcalendar; 4218 * var timezoneComp = vcalendar.getFirstSubcomponent('vtimezone'); 4219 * var tzid = timezoneComp.getFirstPropertyValue('tzid'); 4220 * 4221 * var timezone = new ICAL.Timezone({ 4222 * component: timezoneComp, 4223 * tzid 4224 * }); 4225 * 4226 * @class 4227 * @param {ICAL.Component|Object} data options for class 4228 * @param {String|ICAL.Component} data.component 4229 * If data is a simple object, then this member can be set to either a 4230 * string containing the component data, or an already parsed 4231 * ICAL.Component 4232 * @param {String} data.tzid The timezone identifier 4233 * @param {String} data.location The timezone locationw 4234 * @param {String} data.tznames An alternative string representation of the 4235 * timezone 4236 * @param {Number} data.latitude The latitude of the timezone 4237 * @param {Number} data.longitude The longitude of the timezone 4238 */ 4239 ICAL.Timezone = function icaltimezone(data) { 4240 this.wrappedJSObject = this; 4241 this.fromData(data); 4242 }; 4243 4244 ICAL.Timezone.prototype = { 4245 4246 /** 4247 * Timezone identifier 4248 * @type {String} 4249 */ 4250 tzid: "", 4251 4252 /** 4253 * Timezone location 4254 * @type {String} 4255 */ 4256 location: "", 4257 4258 /** 4259 * Alternative timezone name, for the string representation 4260 * @type {String} 4261 */ 4262 tznames: "", 4263 4264 /** 4265 * The primary latitude for the timezone. 4266 * @type {Number} 4267 */ 4268 latitude: 0.0, 4269 4270 /** 4271 * The primary longitude for the timezone. 4272 * @type {Number} 4273 */ 4274 longitude: 0.0, 4275 4276 /** 4277 * The vtimezone component for this timezone. 4278 * @type {ICAL.Component} 4279 */ 4280 component: null, 4281 4282 /** 4283 * The year this timezone has been expanded to. All timezone transition 4284 * dates until this year are known and can be used for calculation 4285 * 4286 * @private 4287 * @type {Number} 4288 */ 4289 expandedUntilYear: 0, 4290 4291 /** 4292 * The class identifier. 4293 * @constant 4294 * @type {String} 4295 * @default "icaltimezone" 4296 */ 4297 icalclass: "icaltimezone", 4298 4299 /** 4300 * Sets up the current instance using members from the passed data object. 4301 * 4302 * @param {ICAL.Component|Object} aData options for class 4303 * @param {String|ICAL.Component} aData.component 4304 * If aData is a simple object, then this member can be set to either a 4305 * string containing the component data, or an already parsed 4306 * ICAL.Component 4307 * @param {String} aData.tzid The timezone identifier 4308 * @param {String} aData.location The timezone locationw 4309 * @param {String} aData.tznames An alternative string representation of the 4310 * timezone 4311 * @param {Number} aData.latitude The latitude of the timezone 4312 * @param {Number} aData.longitude The longitude of the timezone 4313 */ 4314 fromData: function fromData(aData) { 4315 this.expandedUntilYear = 0; 4316 this.changes = []; 4317 4318 if (aData instanceof ICAL.Component) { 4319 // Either a component is passed directly 4320 this.component = aData; 4321 } else { 4322 // Otherwise the component may be in the data object 4323 if (aData && "component" in aData) { 4324 if (typeof aData.component == "string") { 4325 // If a string was passed, parse it as a component 4326 var jCal = ICAL.parse(aData.component); 4327 this.component = new ICAL.Component(jCal); 4328 } else if (aData.component instanceof ICAL.Component) { 4329 // If it was a component already, then just set it 4330 this.component = aData.component; 4331 } else { 4332 // Otherwise just null out the component 4333 this.component = null; 4334 } 4335 } 4336 4337 // Copy remaining passed properties 4338 for (var key in OPTIONS) { 4339 /* istanbul ignore else */ 4340 if (OPTIONS.hasOwnProperty(key)) { 4341 var prop = OPTIONS[key]; 4342 if (aData && prop in aData) { 4343 this[prop] = aData[prop]; 4344 } 4345 } 4346 } 4347 } 4348 4349 // If we have a component but no TZID, attempt to get it from the 4350 // component's properties. 4351 if (this.component instanceof ICAL.Component && !this.tzid) { 4352 this.tzid = this.component.getFirstPropertyValue('tzid'); 4353 } 4354 4355 return this; 4356 }, 4357 4358 /** 4359 * Finds the utcOffset the given time would occur in this timezone. 4360 * 4361 * @param {ICAL.Time} tt The time to check for 4362 * @return {Number} utc offset in seconds 4363 */ 4364 utcOffset: function utcOffset(tt) { 4365 if (this == ICAL.Timezone.utcTimezone || this == ICAL.Timezone.localTimezone) { 4366 return 0; 4367 } 4368 4369 this._ensureCoverage(tt.year); 4370 4371 if (!this.changes.length) { 4372 return 0; 4373 } 4374 4375 var tt_change = { 4376 year: tt.year, 4377 month: tt.month, 4378 day: tt.day, 4379 hour: tt.hour, 4380 minute: tt.minute, 4381 second: tt.second 4382 }; 4383 4384 var change_num = this._findNearbyChange(tt_change); 4385 var change_num_to_use = -1; 4386 var step = 1; 4387 4388 // TODO: replace with bin search? 4389 for (;;) { 4390 var change = ICAL.helpers.clone(this.changes[change_num], true); 4391 if (change.utcOffset < change.prevUtcOffset) { 4392 ICAL.Timezone.adjust_change(change, 0, 0, 0, change.utcOffset); 4393 } else { 4394 ICAL.Timezone.adjust_change(change, 0, 0, 0, 4395 change.prevUtcOffset); 4396 } 4397 4398 var cmp = ICAL.Timezone._compare_change_fn(tt_change, change); 4399 4400 if (cmp >= 0) { 4401 change_num_to_use = change_num; 4402 } else { 4403 step = -1; 4404 } 4405 4406 if (step == -1 && change_num_to_use != -1) { 4407 break; 4408 } 4409 4410 change_num += step; 4411 4412 if (change_num < 0) { 4413 return 0; 4414 } 4415 4416 if (change_num >= this.changes.length) { 4417 break; 4418 } 4419 } 4420 4421 var zone_change = this.changes[change_num_to_use]; 4422 var utcOffset_change = zone_change.utcOffset - zone_change.prevUtcOffset; 4423 4424 if (utcOffset_change < 0 && change_num_to_use > 0) { 4425 var tmp_change = ICAL.helpers.clone(zone_change, true); 4426 ICAL.Timezone.adjust_change(tmp_change, 0, 0, 0, 4427 tmp_change.prevUtcOffset); 4428 4429 if (ICAL.Timezone._compare_change_fn(tt_change, tmp_change) < 0) { 4430 var prev_zone_change = this.changes[change_num_to_use - 1]; 4431 4432 var want_daylight = false; // TODO 4433 4434 if (zone_change.is_daylight != want_daylight && 4435 prev_zone_change.is_daylight == want_daylight) { 4436 zone_change = prev_zone_change; 4437 } 4438 } 4439 } 4440 4441 // TODO return is_daylight? 4442 return zone_change.utcOffset; 4443 }, 4444 4445 _findNearbyChange: function icaltimezone_find_nearby_change(change) { 4446 // find the closest match 4447 var idx = ICAL.helpers.binsearchInsert( 4448 this.changes, 4449 change, 4450 ICAL.Timezone._compare_change_fn 4451 ); 4452 4453 if (idx >= this.changes.length) { 4454 return this.changes.length - 1; 4455 } 4456 4457 return idx; 4458 }, 4459 4460 _ensureCoverage: function(aYear) { 4461 if (ICAL.Timezone._minimumExpansionYear == -1) { 4462 var today = ICAL.Time.now(); 4463 ICAL.Timezone._minimumExpansionYear = today.year; 4464 } 4465 4466 var changesEndYear = aYear; 4467 if (changesEndYear < ICAL.Timezone._minimumExpansionYear) { 4468 changesEndYear = ICAL.Timezone._minimumExpansionYear; 4469 } 4470 4471 changesEndYear += ICAL.Timezone.EXTRA_COVERAGE; 4472 4473 if (changesEndYear > ICAL.Timezone.MAX_YEAR) { 4474 changesEndYear = ICAL.Timezone.MAX_YEAR; 4475 } 4476 4477 if (!this.changes.length || this.expandedUntilYear < aYear) { 4478 var subcomps = this.component.getAllSubcomponents(); 4479 var compLen = subcomps.length; 4480 var compIdx = 0; 4481 4482 for (; compIdx < compLen; compIdx++) { 4483 this._expandComponent( 4484 subcomps[compIdx], changesEndYear, this.changes 4485 ); 4486 } 4487 4488 this.changes.sort(ICAL.Timezone._compare_change_fn); 4489 this.expandedUntilYear = changesEndYear; 4490 } 4491 }, 4492 4493 _expandComponent: function(aComponent, aYear, changes) { 4494 if (!aComponent.hasProperty("dtstart") || 4495 !aComponent.hasProperty("tzoffsetto") || 4496 !aComponent.hasProperty("tzoffsetfrom")) { 4497 return null; 4498 } 4499 4500 var dtstart = aComponent.getFirstProperty("dtstart").getFirstValue(); 4501 var change; 4502 4503 function convert_tzoffset(offset) { 4504 return offset.factor * (offset.hours * 3600 + offset.minutes * 60); 4505 } 4506 4507 function init_changes() { 4508 var changebase = {}; 4509 changebase.is_daylight = (aComponent.name == "daylight"); 4510 changebase.utcOffset = convert_tzoffset( 4511 aComponent.getFirstProperty("tzoffsetto").getFirstValue() 4512 ); 4513 4514 changebase.prevUtcOffset = convert_tzoffset( 4515 aComponent.getFirstProperty("tzoffsetfrom").getFirstValue() 4516 ); 4517 4518 return changebase; 4519 } 4520 4521 if (!aComponent.hasProperty("rrule") && !aComponent.hasProperty("rdate")) { 4522 change = init_changes(); 4523 change.year = dtstart.year; 4524 change.month = dtstart.month; 4525 change.day = dtstart.day; 4526 change.hour = dtstart.hour; 4527 change.minute = dtstart.minute; 4528 change.second = dtstart.second; 4529 4530 ICAL.Timezone.adjust_change(change, 0, 0, 0, 4531 -change.prevUtcOffset); 4532 changes.push(change); 4533 } else { 4534 var props = aComponent.getAllProperties("rdate"); 4535 for (var rdatekey in props) { 4536 /* istanbul ignore if */ 4537 if (!props.hasOwnProperty(rdatekey)) { 4538 continue; 4539 } 4540 var rdate = props[rdatekey]; 4541 var time = rdate.getFirstValue(); 4542 change = init_changes(); 4543 4544 change.year = time.year; 4545 change.month = time.month; 4546 change.day = time.day; 4547 4548 if (time.isDate) { 4549 change.hour = dtstart.hour; 4550 change.minute = dtstart.minute; 4551 change.second = dtstart.second; 4552 4553 if (dtstart.zone != ICAL.Timezone.utcTimezone) { 4554 ICAL.Timezone.adjust_change(change, 0, 0, 0, 4555 -change.prevUtcOffset); 4556 } 4557 } else { 4558 change.hour = time.hour; 4559 change.minute = time.minute; 4560 change.second = time.second; 4561 4562 if (time.zone != ICAL.Timezone.utcTimezone) { 4563 ICAL.Timezone.adjust_change(change, 0, 0, 0, 4564 -change.prevUtcOffset); 4565 } 4566 } 4567 4568 changes.push(change); 4569 } 4570 4571 var rrule = aComponent.getFirstProperty("rrule"); 4572 4573 if (rrule) { 4574 rrule = rrule.getFirstValue(); 4575 change = init_changes(); 4576 4577 if (rrule.until && rrule.until.zone == ICAL.Timezone.utcTimezone) { 4578 rrule.until.adjust(0, 0, 0, change.prevUtcOffset); 4579 rrule.until.zone = ICAL.Timezone.localTimezone; 4580 } 4581 4582 var iterator = rrule.iterator(dtstart); 4583 4584 var occ; 4585 while ((occ = iterator.next())) { 4586 change = init_changes(); 4587 if (occ.year > aYear || !occ) { 4588 break; 4589 } 4590 4591 change.year = occ.year; 4592 change.month = occ.month; 4593 change.day = occ.day; 4594 change.hour = occ.hour; 4595 change.minute = occ.minute; 4596 change.second = occ.second; 4597 change.isDate = occ.isDate; 4598 4599 ICAL.Timezone.adjust_change(change, 0, 0, 0, 4600 -change.prevUtcOffset); 4601 changes.push(change); 4602 } 4603 } 4604 } 4605 4606 return changes; 4607 }, 4608 4609 /** 4610 * The string representation of this timezone. 4611 * @return {String} 4612 */ 4613 toString: function toString() { 4614 return (this.tznames ? this.tznames : this.tzid); 4615 } 4616 }; 4617 4618 ICAL.Timezone._compare_change_fn = function icaltimezone_compare_change_fn(a, b) { 4619 if (a.year < b.year) return -1; 4620 else if (a.year > b.year) return 1; 4621 4622 if (a.month < b.month) return -1; 4623 else if (a.month > b.month) return 1; 4624 4625 if (a.day < b.day) return -1; 4626 else if (a.day > b.day) return 1; 4627 4628 if (a.hour < b.hour) return -1; 4629 else if (a.hour > b.hour) return 1; 4630 4631 if (a.minute < b.minute) return -1; 4632 else if (a.minute > b.minute) return 1; 4633 4634 if (a.second < b.second) return -1; 4635 else if (a.second > b.second) return 1; 4636 4637 return 0; 4638 }; 4639 4640 /** 4641 * Convert the date/time from one zone to the next. 4642 * 4643 * @param {ICAL.Time} tt The time to convert 4644 * @param {ICAL.Timezone} from_zone The source zone to convert from 4645 * @param {ICAL.Timezone} to_zone The target zone to convert to 4646 * @return {ICAL.Time} The converted date/time object 4647 */ 4648 ICAL.Timezone.convert_time = function icaltimezone_convert_time(tt, from_zone, to_zone) { 4649 if (tt.isDate || 4650 from_zone.tzid == to_zone.tzid || 4651 from_zone == ICAL.Timezone.localTimezone || 4652 to_zone == ICAL.Timezone.localTimezone) { 4653 tt.zone = to_zone; 4654 return tt; 4655 } 4656 4657 var utcOffset = from_zone.utcOffset(tt); 4658 tt.adjust(0, 0, 0, - utcOffset); 4659 4660 utcOffset = to_zone.utcOffset(tt); 4661 tt.adjust(0, 0, 0, utcOffset); 4662 4663 return null; 4664 }; 4665 4666 /** 4667 * Creates a new ICAL.Timezone instance from the passed data object. 4668 * 4669 * @param {ICAL.Component|Object} aData options for class 4670 * @param {String|ICAL.Component} aData.component 4671 * If aData is a simple object, then this member can be set to either a 4672 * string containing the component data, or an already parsed 4673 * ICAL.Component 4674 * @param {String} aData.tzid The timezone identifier 4675 * @param {String} aData.location The timezone locationw 4676 * @param {String} aData.tznames An alternative string representation of the 4677 * timezone 4678 * @param {Number} aData.latitude The latitude of the timezone 4679 * @param {Number} aData.longitude The longitude of the timezone 4680 */ 4681 ICAL.Timezone.fromData = function icaltimezone_fromData(aData) { 4682 var tt = new ICAL.Timezone(); 4683 return tt.fromData(aData); 4684 }; 4685 4686 /** 4687 * The instance describing the UTC timezone 4688 * @type {ICAL.Timezone} 4689 * @constant 4690 * @instance 4691 */ 4692 ICAL.Timezone.utcTimezone = ICAL.Timezone.fromData({ 4693 tzid: "UTC" 4694 }); 4695 4696 /** 4697 * The instance describing the local timezone 4698 * @type {ICAL.Timezone} 4699 * @constant 4700 * @instance 4701 */ 4702 ICAL.Timezone.localTimezone = ICAL.Timezone.fromData({ 4703 tzid: "floating" 4704 }); 4705 4706 /** 4707 * Adjust a timezone change object. 4708 * @private 4709 * @param {Object} change The timezone change object 4710 * @param {Number} days The extra amount of days 4711 * @param {Number} hours The extra amount of hours 4712 * @param {Number} minutes The extra amount of minutes 4713 * @param {Number} seconds The extra amount of seconds 4714 */ 4715 ICAL.Timezone.adjust_change = function icaltimezone_adjust_change(change, days, hours, minutes, seconds) { 4716 return ICAL.Time.prototype.adjust.call( 4717 change, 4718 days, 4719 hours, 4720 minutes, 4721 seconds, 4722 change 4723 ); 4724 }; 4725 4726 ICAL.Timezone._minimumExpansionYear = -1; 4727 ICAL.Timezone.MAX_YEAR = 2035; // TODO this is because of time_t, which we don't need. Still usefull? 4728 ICAL.Timezone.EXTRA_COVERAGE = 5; 4729})(); 4730/* This Source Code Form is subject to the terms of the Mozilla Public 4731 * License, v. 2.0. If a copy of the MPL was not distributed with this 4732 * file, You can obtain one at http://mozilla.org/MPL/2.0/. 4733 * Portions Copyright (C) Philipp Kewisch, 2011-2015 */ 4734 4735 4736/** 4737 * This symbol is further described later on 4738 * @ignore 4739 */ 4740ICAL.TimezoneService = (function() { 4741 var zones; 4742 4743 /** 4744 * @classdesc 4745 * Singleton class to contain timezones. Right now its all manual registry in 4746 * the future we may use this class to download timezone information or handle 4747 * loading pre-expanded timezones. 4748 * 4749 * @namespace 4750 * @alias ICAL.TimezoneService 4751 */ 4752 var TimezoneService = { 4753 get count() { 4754 return Object.keys(zones).length; 4755 }, 4756 4757 reset: function() { 4758 zones = Object.create(null); 4759 var utc = ICAL.Timezone.utcTimezone; 4760 4761 zones.Z = utc; 4762 zones.UTC = utc; 4763 zones.GMT = utc; 4764 }, 4765 4766 /** 4767 * Checks if timezone id has been registered. 4768 * 4769 * @param {String} tzid Timezone identifier (e.g. America/Los_Angeles) 4770 * @return {Boolean} False, when not present 4771 */ 4772 has: function(tzid) { 4773 return !!zones[tzid]; 4774 }, 4775 4776 /** 4777 * Returns a timezone by its tzid if present. 4778 * 4779 * @param {String} tzid Timezone identifier (e.g. America/Los_Angeles) 4780 * @return {?ICAL.Timezone} The timezone, or null if not found 4781 */ 4782 get: function(tzid) { 4783 return zones[tzid]; 4784 }, 4785 4786 /** 4787 * Registers a timezone object or component. 4788 * 4789 * @param {String=} name 4790 * The name of the timezone. Defaults to the component's TZID if not 4791 * passed. 4792 * @param {ICAL.Component|ICAL.Timezone} zone 4793 * The initialized zone or vtimezone. 4794 */ 4795 register: function(name, timezone) { 4796 if (name instanceof ICAL.Component) { 4797 if (name.name === 'vtimezone') { 4798 timezone = new ICAL.Timezone(name); 4799 name = timezone.tzid; 4800 } 4801 } 4802 4803 if (timezone instanceof ICAL.Timezone) { 4804 zones[name] = timezone; 4805 } else { 4806 throw new TypeError('timezone must be ICAL.Timezone or ICAL.Component'); 4807 } 4808 }, 4809 4810 /** 4811 * Removes a timezone by its tzid from the list. 4812 * 4813 * @param {String} tzid Timezone identifier (e.g. America/Los_Angeles) 4814 * @return {?ICAL.Timezone} The removed timezone, or null if not registered 4815 */ 4816 remove: function(tzid) { 4817 return (delete zones[tzid]); 4818 } 4819 }; 4820 4821 // initialize defaults 4822 TimezoneService.reset(); 4823 4824 return TimezoneService; 4825}()); 4826/* This Source Code Form is subject to the terms of the Mozilla Public 4827 * License, v. 2.0. If a copy of the MPL was not distributed with this 4828 * file, You can obtain one at http://mozilla.org/MPL/2.0/. 4829 * Portions Copyright (C) Philipp Kewisch, 2011-2015 */ 4830 4831 4832 4833(function() { 4834 4835 /** 4836 * @classdesc 4837 * iCalendar Time representation (similar to JS Date object). Fully 4838 * independent of system (OS) timezone / time. Unlike JS Date, the month 4839 * January is 1, not zero. 4840 * 4841 * @example 4842 * var time = new ICAL.Time({ 4843 * year: 2012, 4844 * month: 10, 4845 * day: 11 4846 * minute: 0, 4847 * second: 0, 4848 * isDate: false 4849 * }); 4850 * 4851 * 4852 * @alias ICAL.Time 4853 * @class 4854 * @param {Object} data Time initialization 4855 * @param {Number=} data.year The year for this date 4856 * @param {Number=} data.month The month for this date 4857 * @param {Number=} data.day The day for this date 4858 * @param {Number=} data.hour The hour for this date 4859 * @param {Number=} data.minute The minute for this date 4860 * @param {Number=} data.second The second for this date 4861 * @param {Boolean=} data.isDate If true, the instance represents a date (as 4862 * opposed to a date-time) 4863 * @param {ICAL.Timezone} zone timezone this position occurs in 4864 */ 4865 ICAL.Time = function icaltime(data, zone) { 4866 this.wrappedJSObject = this; 4867 var time = this._time = Object.create(null); 4868 4869 /* time defaults */ 4870 time.year = 0; 4871 time.month = 1; 4872 time.day = 1; 4873 time.hour = 0; 4874 time.minute = 0; 4875 time.second = 0; 4876 time.isDate = false; 4877 4878 this.fromData(data, zone); 4879 }; 4880 4881 ICAL.Time._dowCache = {}; 4882 ICAL.Time._wnCache = {}; 4883 4884 ICAL.Time.prototype = { 4885 4886 /** 4887 * The class identifier. 4888 * @constant 4889 * @type {String} 4890 * @default "icaltime" 4891 */ 4892 icalclass: "icaltime", 4893 _cachedUnixTime: null, 4894 4895 /** 4896 * The type name, to be used in the jCal object. This value may change and 4897 * is strictly defined by the {@link ICAL.Time#isDate isDate} member. 4898 * @readonly 4899 * @type {String} 4900 * @default "date-time" 4901 */ 4902 get icaltype() { 4903 return this.isDate ? 'date' : 'date-time'; 4904 }, 4905 4906 /** 4907 * The timezone for this time. 4908 * @type {ICAL.Timezone} 4909 */ 4910 zone: null, 4911 4912 /** 4913 * Internal uses to indicate that a change has been made and the next read 4914 * operation must attempt to normalize the value (for example changing the 4915 * day to 33). 4916 * 4917 * @type {Boolean} 4918 * @private 4919 */ 4920 _pendingNormalization: false, 4921 4922 /** 4923 * Returns a clone of the time object. 4924 * 4925 * @return {ICAL.Time} The cloned object 4926 */ 4927 clone: function() { 4928 return new ICAL.Time(this._time, this.zone); 4929 }, 4930 4931 /** 4932 * Reset the time instance to epoch time 4933 */ 4934 reset: function icaltime_reset() { 4935 this.fromData(ICAL.Time.epochTime); 4936 this.zone = ICAL.Timezone.utcTimezone; 4937 }, 4938 4939 /** 4940 * Reset the time instance to the given date/time values. 4941 * 4942 * @param {Number} year The year to set 4943 * @param {Number} month The month to set 4944 * @param {Number} day The day to set 4945 * @param {Number} hour The hour to set 4946 * @param {Number} minute The minute to set 4947 * @param {Number} second The second to set 4948 * @param {ICAL.Timezone} timezone The timezone to set 4949 */ 4950 resetTo: function icaltime_resetTo(year, month, day, 4951 hour, minute, second, timezone) { 4952 this.fromData({ 4953 year: year, 4954 month: month, 4955 day: day, 4956 hour: hour, 4957 minute: minute, 4958 second: second, 4959 zone: timezone 4960 }); 4961 }, 4962 4963 /** 4964 * Set up the current instance from the Javascript date value. 4965 * 4966 * @param {?Date} aDate The Javascript Date to read, or null to reset 4967 * @param {Boolean} useUTC If true, the UTC values of the date will be used 4968 */ 4969 fromJSDate: function icaltime_fromJSDate(aDate, useUTC) { 4970 if (!aDate) { 4971 this.reset(); 4972 } else { 4973 if (useUTC) { 4974 this.zone = ICAL.Timezone.utcTimezone; 4975 this.year = aDate.getUTCFullYear(); 4976 this.month = aDate.getUTCMonth() + 1; 4977 this.day = aDate.getUTCDate(); 4978 this.hour = aDate.getUTCHours(); 4979 this.minute = aDate.getUTCMinutes(); 4980 this.second = aDate.getUTCSeconds(); 4981 } else { 4982 this.zone = ICAL.Timezone.localTimezone; 4983 this.year = aDate.getFullYear(); 4984 this.month = aDate.getMonth() + 1; 4985 this.day = aDate.getDate(); 4986 this.hour = aDate.getHours(); 4987 this.minute = aDate.getMinutes(); 4988 this.second = aDate.getSeconds(); 4989 } 4990 } 4991 this._cachedUnixTime = null; 4992 return this; 4993 }, 4994 4995 /** 4996 * Sets up the current instance using members from the passed data object. 4997 * 4998 * @param {Object} aData Time initialization 4999 * @param {Number=} aData.year The year for this date 5000 * @param {Number=} aData.month The month for this date 5001 * @param {Number=} aData.day The day for this date 5002 * @param {Number=} aData.hour The hour for this date 5003 * @param {Number=} aData.minute The minute for this date 5004 * @param {Number=} aData.second The second for this date 5005 * @param {Boolean=} aData.isDate If true, the instance represents a date 5006 * (as opposed to a date-time) 5007 * @param {ICAL.Timezone=} aZone Timezone this position occurs in 5008 */ 5009 fromData: function fromData(aData, aZone) { 5010 if (aData) { 5011 for (var key in aData) { 5012 /* istanbul ignore else */ 5013 if (Object.prototype.hasOwnProperty.call(aData, key)) { 5014 // ical type cannot be set 5015 if (key === 'icaltype') continue; 5016 this[key] = aData[key]; 5017 } 5018 } 5019 } 5020 5021 if (aZone) { 5022 this.zone = aZone; 5023 } 5024 5025 if (aData && !("isDate" in aData)) { 5026 this.isDate = !("hour" in aData); 5027 } else if (aData && ("isDate" in aData)) { 5028 this.isDate = aData.isDate; 5029 } 5030 5031 if (aData && "timezone" in aData) { 5032 var zone = ICAL.TimezoneService.get( 5033 aData.timezone 5034 ); 5035 5036 this.zone = zone || ICAL.Timezone.localTimezone; 5037 } 5038 5039 if (aData && "zone" in aData) { 5040 this.zone = aData.zone; 5041 } 5042 5043 if (!this.zone) { 5044 this.zone = ICAL.Timezone.localTimezone; 5045 } 5046 5047 this._cachedUnixTime = null; 5048 return this; 5049 }, 5050 5051 /** 5052 * Calculate the day of week. 5053 * @param {ICAL.Time.weekDay=} aWeekStart 5054 * The week start weekday, defaults to SUNDAY 5055 * @return {ICAL.Time.weekDay} 5056 */ 5057 dayOfWeek: function icaltime_dayOfWeek(aWeekStart) { 5058 var firstDow = aWeekStart || ICAL.Time.SUNDAY; 5059 var dowCacheKey = (this.year << 12) + (this.month << 8) + (this.day << 3) + firstDow; 5060 if (dowCacheKey in ICAL.Time._dowCache) { 5061 return ICAL.Time._dowCache[dowCacheKey]; 5062 } 5063 5064 // Using Zeller's algorithm 5065 var q = this.day; 5066 var m = this.month + (this.month < 3 ? 12 : 0); 5067 var Y = this.year - (this.month < 3 ? 1 : 0); 5068 5069 var h = (q + Y + ICAL.helpers.trunc(((m + 1) * 26) / 10) + ICAL.helpers.trunc(Y / 4)); 5070 /* istanbul ignore else */ 5071 if (true /* gregorian */) { 5072 h += ICAL.helpers.trunc(Y / 100) * 6 + ICAL.helpers.trunc(Y / 400); 5073 } else { 5074 h += 5; 5075 } 5076 5077 // Normalize to 1 = wkst 5078 h = ((h + 7 - firstDow) % 7) + 1; 5079 ICAL.Time._dowCache[dowCacheKey] = h; 5080 return h; 5081 }, 5082 5083 /** 5084 * Calculate the day of year. 5085 * @return {Number} 5086 */ 5087 dayOfYear: function dayOfYear() { 5088 var is_leap = (ICAL.Time.isLeapYear(this.year) ? 1 : 0); 5089 var diypm = ICAL.Time.daysInYearPassedMonth; 5090 return diypm[is_leap][this.month - 1] + this.day; 5091 }, 5092 5093 /** 5094 * Returns a copy of the current date/time, rewound to the start of the 5095 * week. The resulting ICAL.Time instance is of icaltype date, even if this 5096 * is a date-time. 5097 * 5098 * @param {ICAL.Time.weekDay=} aWeekStart 5099 * The week start weekday, defaults to SUNDAY 5100 * @return {ICAL.Time} The start of the week (cloned) 5101 */ 5102 startOfWeek: function startOfWeek(aWeekStart) { 5103 var firstDow = aWeekStart || ICAL.Time.SUNDAY; 5104 var result = this.clone(); 5105 result.day -= ((this.dayOfWeek() + 7 - firstDow) % 7); 5106 result.isDate = true; 5107 result.hour = 0; 5108 result.minute = 0; 5109 result.second = 0; 5110 return result; 5111 }, 5112 5113 /** 5114 * Returns a copy of the current date/time, shifted to the end of the week. 5115 * The resulting ICAL.Time instance is of icaltype date, even if this is a 5116 * date-time. 5117 * 5118 * @param {ICAL.Time.weekDay=} aWeekStart 5119 * The week start weekday, defaults to SUNDAY 5120 * @return {ICAL.Time} The end of the week (cloned) 5121 */ 5122 endOfWeek: function endOfWeek(aWeekStart) { 5123 var firstDow = aWeekStart || ICAL.Time.SUNDAY; 5124 var result = this.clone(); 5125 result.day += (7 - this.dayOfWeek() + firstDow - ICAL.Time.SUNDAY) % 7; 5126 result.isDate = true; 5127 result.hour = 0; 5128 result.minute = 0; 5129 result.second = 0; 5130 return result; 5131 }, 5132 5133 /** 5134 * Returns a copy of the current date/time, rewound to the start of the 5135 * month. The resulting ICAL.Time instance is of icaltype date, even if 5136 * this is a date-time. 5137 * 5138 * @return {ICAL.Time} The start of the month (cloned) 5139 */ 5140 startOfMonth: function startOfMonth() { 5141 var result = this.clone(); 5142 result.day = 1; 5143 result.isDate = true; 5144 result.hour = 0; 5145 result.minute = 0; 5146 result.second = 0; 5147 return result; 5148 }, 5149 5150 /** 5151 * Returns a copy of the current date/time, shifted to the end of the 5152 * month. The resulting ICAL.Time instance is of icaltype date, even if 5153 * this is a date-time. 5154 * 5155 * @return {ICAL.Time} The end of the month (cloned) 5156 */ 5157 endOfMonth: function endOfMonth() { 5158 var result = this.clone(); 5159 result.day = ICAL.Time.daysInMonth(result.month, result.year); 5160 result.isDate = true; 5161 result.hour = 0; 5162 result.minute = 0; 5163 result.second = 0; 5164 return result; 5165 }, 5166 5167 /** 5168 * Returns a copy of the current date/time, rewound to the start of the 5169 * year. The resulting ICAL.Time instance is of icaltype date, even if 5170 * this is a date-time. 5171 * 5172 * @return {ICAL.Time} The start of the year (cloned) 5173 */ 5174 startOfYear: function startOfYear() { 5175 var result = this.clone(); 5176 result.day = 1; 5177 result.month = 1; 5178 result.isDate = true; 5179 result.hour = 0; 5180 result.minute = 0; 5181 result.second = 0; 5182 return result; 5183 }, 5184 5185 /** 5186 * Returns a copy of the current date/time, shifted to the end of the 5187 * year. The resulting ICAL.Time instance is of icaltype date, even if 5188 * this is a date-time. 5189 * 5190 * @return {ICAL.Time} The end of the year (cloned) 5191 */ 5192 endOfYear: function endOfYear() { 5193 var result = this.clone(); 5194 result.day = 31; 5195 result.month = 12; 5196 result.isDate = true; 5197 result.hour = 0; 5198 result.minute = 0; 5199 result.second = 0; 5200 return result; 5201 }, 5202 5203 /** 5204 * First calculates the start of the week, then returns the day of year for 5205 * this date. If the day falls into the previous year, the day is zero or negative. 5206 * 5207 * @param {ICAL.Time.weekDay=} aFirstDayOfWeek 5208 * The week start weekday, defaults to SUNDAY 5209 * @return {Number} The calculated day of year 5210 */ 5211 startDoyWeek: function startDoyWeek(aFirstDayOfWeek) { 5212 var firstDow = aFirstDayOfWeek || ICAL.Time.SUNDAY; 5213 var delta = this.dayOfWeek() - firstDow; 5214 if (delta < 0) delta += 7; 5215 return this.dayOfYear() - delta; 5216 }, 5217 5218 /** 5219 * Get the dominical letter for the current year. Letters range from A - G 5220 * for common years, and AG to GF for leap years. 5221 * 5222 * @param {Number} yr The year to retrieve the letter for 5223 * @return {String} The dominical letter. 5224 */ 5225 getDominicalLetter: function() { 5226 return ICAL.Time.getDominicalLetter(this.year); 5227 }, 5228 5229 /** 5230 * Finds the nthWeekDay relative to the current month (not day). The 5231 * returned value is a day relative the month that this month belongs to so 5232 * 1 would indicate the first of the month and 40 would indicate a day in 5233 * the following month. 5234 * 5235 * @param {Number} aDayOfWeek Day of the week see the day name constants 5236 * @param {Number} aPos Nth occurrence of a given week day values 5237 * of 1 and 0 both indicate the first weekday of that type. aPos may 5238 * be either positive or negative 5239 * 5240 * @return {Number} numeric value indicating a day relative 5241 * to the current month of this time object 5242 */ 5243 nthWeekDay: function icaltime_nthWeekDay(aDayOfWeek, aPos) { 5244 var daysInMonth = ICAL.Time.daysInMonth(this.month, this.year); 5245 var weekday; 5246 var pos = aPos; 5247 5248 var start = 0; 5249 5250 var otherDay = this.clone(); 5251 5252 if (pos >= 0) { 5253 otherDay.day = 1; 5254 5255 // because 0 means no position has been given 5256 // 1 and 0 indicate the same day. 5257 if (pos != 0) { 5258 // remove the extra numeric value 5259 pos--; 5260 } 5261 5262 // set current start offset to current day. 5263 start = otherDay.day; 5264 5265 // find the current day of week 5266 var startDow = otherDay.dayOfWeek(); 5267 5268 // calculate the difference between current 5269 // day of the week and desired day of the week 5270 var offset = aDayOfWeek - startDow; 5271 5272 5273 // if the offset goes into the past 5274 // week we add 7 so its goes into the next 5275 // week. We only want to go forward in time here. 5276 if (offset < 0) 5277 // this is really important otherwise we would 5278 // end up with dates from in the past. 5279 offset += 7; 5280 5281 // add offset to start so start is the same 5282 // day of the week as the desired day of week. 5283 start += offset; 5284 5285 // because we are going to add (and multiply) 5286 // the numeric value of the day we subtract it 5287 // from the start position so not to add it twice. 5288 start -= aDayOfWeek; 5289 5290 // set week day 5291 weekday = aDayOfWeek; 5292 } else { 5293 5294 // then we set it to the last day in the current month 5295 otherDay.day = daysInMonth; 5296 5297 // find the ends weekday 5298 var endDow = otherDay.dayOfWeek(); 5299 5300 pos++; 5301 5302 weekday = (endDow - aDayOfWeek); 5303 5304 if (weekday < 0) { 5305 weekday += 7; 5306 } 5307 5308 weekday = daysInMonth - weekday; 5309 } 5310 5311 weekday += pos * 7; 5312 5313 return start + weekday; 5314 }, 5315 5316 /** 5317 * Checks if current time is the nth weekday, relative to the current 5318 * month. Will always return false when rule resolves outside of current 5319 * month. 5320 * 5321 * @param {ICAL.Time.weekDay} aDayOfWeek Day of week to check 5322 * @param {Number} aPos Relative position 5323 * @return {Boolean} True, if its the nth weekday 5324 */ 5325 isNthWeekDay: function(aDayOfWeek, aPos) { 5326 var dow = this.dayOfWeek(); 5327 5328 if (aPos === 0 && dow === aDayOfWeek) { 5329 return true; 5330 } 5331 5332 // get pos 5333 var day = this.nthWeekDay(aDayOfWeek, aPos); 5334 5335 if (day === this.day) { 5336 return true; 5337 } 5338 5339 return false; 5340 }, 5341 5342 /** 5343 * Calculates the ISO 8601 week number. The first week of a year is the 5344 * week that contains the first Thursday. The year can have 53 weeks, if 5345 * January 1st is a Friday. 5346 * 5347 * Note there are regions where the first week of the year is the one that 5348 * starts on January 1st, which may offset the week number. Also, if a 5349 * different week start is specified, this will also affect the week 5350 * number. 5351 * 5352 * @see ICAL.Time.weekOneStarts 5353 * @param {ICAL.Time.weekDay} aWeekStart The weekday the week starts with 5354 * @return {Number} The ISO week number 5355 */ 5356 weekNumber: function weekNumber(aWeekStart) { 5357 var wnCacheKey = (this.year << 12) + (this.month << 8) + (this.day << 3) + aWeekStart; 5358 if (wnCacheKey in ICAL.Time._wnCache) { 5359 return ICAL.Time._wnCache[wnCacheKey]; 5360 } 5361 // This function courtesty of Julian Bucknall, published under the MIT license 5362 // http://www.boyet.com/articles/publishedarticles/calculatingtheisoweeknumb.html 5363 // plus some fixes to be able to use different week starts. 5364 var week1; 5365 5366 var dt = this.clone(); 5367 dt.isDate = true; 5368 var isoyear = this.year; 5369 5370 if (dt.month == 12 && dt.day > 25) { 5371 week1 = ICAL.Time.weekOneStarts(isoyear + 1, aWeekStart); 5372 if (dt.compare(week1) < 0) { 5373 week1 = ICAL.Time.weekOneStarts(isoyear, aWeekStart); 5374 } else { 5375 isoyear++; 5376 } 5377 } else { 5378 week1 = ICAL.Time.weekOneStarts(isoyear, aWeekStart); 5379 if (dt.compare(week1) < 0) { 5380 week1 = ICAL.Time.weekOneStarts(--isoyear, aWeekStart); 5381 } 5382 } 5383 5384 var daysBetween = (dt.subtractDate(week1).toSeconds() / 86400); 5385 var answer = ICAL.helpers.trunc(daysBetween / 7) + 1; 5386 ICAL.Time._wnCache[wnCacheKey] = answer; 5387 return answer; 5388 }, 5389 5390 /** 5391 * Adds the duration to the current time. The instance is modified in 5392 * place. 5393 * 5394 * @param {ICAL.Duration} aDuration The duration to add 5395 */ 5396 addDuration: function icaltime_add(aDuration) { 5397 var mult = (aDuration.isNegative ? -1 : 1); 5398 5399 // because of the duration optimizations it is much 5400 // more efficient to grab all the values up front 5401 // then set them directly (which will avoid a normalization call). 5402 // So we don't actually normalize until we need it. 5403 var second = this.second; 5404 var minute = this.minute; 5405 var hour = this.hour; 5406 var day = this.day; 5407 5408 second += mult * aDuration.seconds; 5409 minute += mult * aDuration.minutes; 5410 hour += mult * aDuration.hours; 5411 day += mult * aDuration.days; 5412 day += mult * 7 * aDuration.weeks; 5413 5414 this.second = second; 5415 this.minute = minute; 5416 this.hour = hour; 5417 this.day = day; 5418 5419 this._cachedUnixTime = null; 5420 }, 5421 5422 /** 5423 * Subtract the date details (_excluding_ timezone). Useful for finding 5424 * the relative difference between two time objects excluding their 5425 * timezone differences. 5426 * 5427 * @param {ICAL.Time} aDate The date to substract 5428 * @return {ICAL.Duration} The difference as a duration 5429 */ 5430 subtractDate: function icaltime_subtract(aDate) { 5431 var unixTime = this.toUnixTime() + this.utcOffset(); 5432 var other = aDate.toUnixTime() + aDate.utcOffset(); 5433 return ICAL.Duration.fromSeconds(unixTime - other); 5434 }, 5435 5436 /** 5437 * Subtract the date details, taking timezones into account. 5438 * 5439 * @param {ICAL.Time} aDate The date to subtract 5440 * @return {ICAL.Duration} The difference in duration 5441 */ 5442 subtractDateTz: function icaltime_subtract_abs(aDate) { 5443 var unixTime = this.toUnixTime(); 5444 var other = aDate.toUnixTime(); 5445 return ICAL.Duration.fromSeconds(unixTime - other); 5446 }, 5447 5448 /** 5449 * Compares the ICAL.Time instance with another one. 5450 * 5451 * @param {ICAL.Duration} aOther The instance to compare with 5452 * @return {Number} -1, 0 or 1 for less/equal/greater 5453 */ 5454 compare: function icaltime_compare(other) { 5455 var a = this.toUnixTime(); 5456 var b = other.toUnixTime(); 5457 5458 if (a > b) return 1; 5459 if (b > a) return -1; 5460 return 0; 5461 }, 5462 5463 /** 5464 * Compares only the date part of this instance with another one. 5465 * 5466 * @param {ICAL.Duration} other The instance to compare with 5467 * @param {ICAL.Timezone} tz The timezone to compare in 5468 * @return {Number} -1, 0 or 1 for less/equal/greater 5469 */ 5470 compareDateOnlyTz: function icaltime_compareDateOnlyTz(other, tz) { 5471 function cmp(attr) { 5472 return ICAL.Time._cmp_attr(a, b, attr); 5473 } 5474 var a = this.convertToZone(tz); 5475 var b = other.convertToZone(tz); 5476 var rc = 0; 5477 5478 if ((rc = cmp("year")) != 0) return rc; 5479 if ((rc = cmp("month")) != 0) return rc; 5480 if ((rc = cmp("day")) != 0) return rc; 5481 5482 return rc; 5483 }, 5484 5485 /** 5486 * Convert the instance into another timzone. The returned ICAL.Time 5487 * instance is always a copy. 5488 * 5489 * @param {ICAL.Timezone} zone The zone to convert to 5490 * @return {ICAL.Time} The copy, converted to the zone 5491 */ 5492 convertToZone: function convertToZone(zone) { 5493 var copy = this.clone(); 5494 var zone_equals = (this.zone.tzid == zone.tzid); 5495 5496 if (!this.isDate && !zone_equals) { 5497 ICAL.Timezone.convert_time(copy, this.zone, zone); 5498 } 5499 5500 copy.zone = zone; 5501 return copy; 5502 }, 5503 5504 /** 5505 * Calculates the UTC offset of the current date/time in the timezone it is 5506 * in. 5507 * 5508 * @return {Number} UTC offset in seconds 5509 */ 5510 utcOffset: function utc_offset() { 5511 if (this.zone == ICAL.Timezone.localTimezone || 5512 this.zone == ICAL.Timezone.utcTimezone) { 5513 return 0; 5514 } else { 5515 return this.zone.utcOffset(this); 5516 } 5517 }, 5518 5519 /** 5520 * Returns an RFC 5545 compliant ical representation of this object. 5521 * 5522 * @return {String} ical date/date-time 5523 */ 5524 toICALString: function() { 5525 var string = this.toString(); 5526 5527 if (string.length > 10) { 5528 return ICAL.design.icalendar.value['date-time'].toICAL(string); 5529 } else { 5530 return ICAL.design.icalendar.value.date.toICAL(string); 5531 } 5532 }, 5533 5534 /** 5535 * The string representation of this date/time, in jCal form 5536 * (including : and - separators). 5537 * @return {String} 5538 */ 5539 toString: function toString() { 5540 var result = this.year + '-' + 5541 ICAL.helpers.pad2(this.month) + '-' + 5542 ICAL.helpers.pad2(this.day); 5543 5544 if (!this.isDate) { 5545 result += 'T' + ICAL.helpers.pad2(this.hour) + ':' + 5546 ICAL.helpers.pad2(this.minute) + ':' + 5547 ICAL.helpers.pad2(this.second); 5548 5549 if (this.zone === ICAL.Timezone.utcTimezone) { 5550 result += 'Z'; 5551 } 5552 } 5553 5554 return result; 5555 }, 5556 5557 /** 5558 * Converts the current instance to a Javascript date 5559 * @return {Date} 5560 */ 5561 toJSDate: function toJSDate() { 5562 if (this.zone == ICAL.Timezone.localTimezone) { 5563 if (this.isDate) { 5564 return new Date(this.year, this.month - 1, this.day); 5565 } else { 5566 return new Date(this.year, this.month - 1, this.day, 5567 this.hour, this.minute, this.second, 0); 5568 } 5569 } else { 5570 return new Date(this.toUnixTime() * 1000); 5571 } 5572 }, 5573 5574 _normalize: function icaltime_normalize() { 5575 var isDate = this._time.isDate; 5576 if (this._time.isDate) { 5577 this._time.hour = 0; 5578 this._time.minute = 0; 5579 this._time.second = 0; 5580 } 5581 this.adjust(0, 0, 0, 0); 5582 5583 return this; 5584 }, 5585 5586 /** 5587 * Adjust the date/time by the given offset 5588 * 5589 * @param {Number} aExtraDays The extra amount of days 5590 * @param {Number} aExtraHours The extra amount of hours 5591 * @param {Number} aExtraMinutes The extra amount of minutes 5592 * @param {Number} aExtraSeconds The extra amount of seconds 5593 * @param {Number=} aTime The time to adjust, defaults to the 5594 * current instance. 5595 */ 5596 adjust: function icaltime_adjust(aExtraDays, aExtraHours, 5597 aExtraMinutes, aExtraSeconds, aTime) { 5598 5599 var minutesOverflow, hoursOverflow, 5600 daysOverflow = 0, yearsOverflow = 0; 5601 5602 var second, minute, hour, day; 5603 var daysInMonth; 5604 5605 var time = aTime || this._time; 5606 5607 if (!time.isDate) { 5608 second = time.second + aExtraSeconds; 5609 time.second = second % 60; 5610 minutesOverflow = ICAL.helpers.trunc(second / 60); 5611 if (time.second < 0) { 5612 time.second += 60; 5613 minutesOverflow--; 5614 } 5615 5616 minute = time.minute + aExtraMinutes + minutesOverflow; 5617 time.minute = minute % 60; 5618 hoursOverflow = ICAL.helpers.trunc(minute / 60); 5619 if (time.minute < 0) { 5620 time.minute += 60; 5621 hoursOverflow--; 5622 } 5623 5624 hour = time.hour + aExtraHours + hoursOverflow; 5625 5626 time.hour = hour % 24; 5627 daysOverflow = ICAL.helpers.trunc(hour / 24); 5628 if (time.hour < 0) { 5629 time.hour += 24; 5630 daysOverflow--; 5631 } 5632 } 5633 5634 5635 // Adjust month and year first, because we need to know what month the day 5636 // is in before adjusting it. 5637 if (time.month > 12) { 5638 yearsOverflow = ICAL.helpers.trunc((time.month - 1) / 12); 5639 } else if (time.month < 1) { 5640 yearsOverflow = ICAL.helpers.trunc(time.month / 12) - 1; 5641 } 5642 5643 time.year += yearsOverflow; 5644 time.month -= 12 * yearsOverflow; 5645 5646 // Now take care of the days (and adjust month if needed) 5647 day = time.day + aExtraDays + daysOverflow; 5648 5649 if (day > 0) { 5650 for (;;) { 5651 daysInMonth = ICAL.Time.daysInMonth(time.month, time.year); 5652 if (day <= daysInMonth) { 5653 break; 5654 } 5655 5656 time.month++; 5657 if (time.month > 12) { 5658 time.year++; 5659 time.month = 1; 5660 } 5661 5662 day -= daysInMonth; 5663 } 5664 } else { 5665 while (day <= 0) { 5666 if (time.month == 1) { 5667 time.year--; 5668 time.month = 12; 5669 } else { 5670 time.month--; 5671 } 5672 5673 day += ICAL.Time.daysInMonth(time.month, time.year); 5674 } 5675 } 5676 5677 time.day = day; 5678 5679 this._cachedUnixTime = null; 5680 return this; 5681 }, 5682 5683 /** 5684 * Sets up the current instance from unix time, the number of seconds since 5685 * January 1st, 1970. 5686 * 5687 * @param {Number} seconds The seconds to set up with 5688 */ 5689 fromUnixTime: function fromUnixTime(seconds) { 5690 this.zone = ICAL.Timezone.utcTimezone; 5691 var epoch = ICAL.Time.epochTime.clone(); 5692 epoch.adjust(0, 0, 0, seconds); 5693 5694 this.year = epoch.year; 5695 this.month = epoch.month; 5696 this.day = epoch.day; 5697 this.hour = epoch.hour; 5698 this.minute = epoch.minute; 5699 this.second = Math.floor(epoch.second); 5700 5701 this._cachedUnixTime = null; 5702 }, 5703 5704 /** 5705 * Converts the current instance to seconds since January 1st 1970. 5706 * 5707 * @return {Number} Seconds since 1970 5708 */ 5709 toUnixTime: function toUnixTime() { 5710 if (this._cachedUnixTime !== null) { 5711 return this._cachedUnixTime; 5712 } 5713 var offset = this.utcOffset(); 5714 5715 // we use the offset trick to ensure 5716 // that we are getting the actual UTC time 5717 var ms = Date.UTC( 5718 this.year, 5719 this.month - 1, 5720 this.day, 5721 this.hour, 5722 this.minute, 5723 this.second - offset 5724 ); 5725 5726 // seconds 5727 this._cachedUnixTime = ms / 1000; 5728 return this._cachedUnixTime; 5729 }, 5730 5731 /** 5732 * Converts time to into Object which can be serialized then re-created 5733 * using the constructor. 5734 * 5735 * @example 5736 * // toJSON will automatically be called 5737 * var json = JSON.stringify(mytime); 5738 * 5739 * var deserialized = JSON.parse(json); 5740 * 5741 * var time = new ICAL.Time(deserialized); 5742 * 5743 * @return {Object} 5744 */ 5745 toJSON: function() { 5746 var copy = [ 5747 'year', 5748 'month', 5749 'day', 5750 'hour', 5751 'minute', 5752 'second', 5753 'isDate' 5754 ]; 5755 5756 var result = Object.create(null); 5757 5758 var i = 0; 5759 var len = copy.length; 5760 var prop; 5761 5762 for (; i < len; i++) { 5763 prop = copy[i]; 5764 result[prop] = this[prop]; 5765 } 5766 5767 if (this.zone) { 5768 result.timezone = this.zone.tzid; 5769 } 5770 5771 return result; 5772 } 5773 5774 }; 5775 5776 (function setupNormalizeAttributes() { 5777 // This needs to run before any instances are created! 5778 function defineAttr(attr) { 5779 Object.defineProperty(ICAL.Time.prototype, attr, { 5780 get: function getTimeAttr() { 5781 if (this._pendingNormalization) { 5782 this._normalize(); 5783 this._pendingNormalization = false; 5784 } 5785 5786 return this._time[attr]; 5787 }, 5788 set: function setTimeAttr(val) { 5789 // Check if isDate will be set and if was not set to normalize date. 5790 // This avoids losing days when seconds, minutes and hours are zeroed 5791 // what normalize will do when time is a date. 5792 if (attr === "isDate" && val && !this._time.isDate) { 5793 this.adjust(0, 0, 0, 0); 5794 } 5795 this._cachedUnixTime = null; 5796 this._pendingNormalization = true; 5797 this._time[attr] = val; 5798 5799 return val; 5800 } 5801 }); 5802 5803 } 5804 5805 /* istanbul ignore else */ 5806 if ("defineProperty" in Object) { 5807 defineAttr("year"); 5808 defineAttr("month"); 5809 defineAttr("day"); 5810 defineAttr("hour"); 5811 defineAttr("minute"); 5812 defineAttr("second"); 5813 defineAttr("isDate"); 5814 } 5815 })(); 5816 5817 /** 5818 * Returns the days in the given month 5819 * 5820 * @param {Number} month The month to check 5821 * @param {Number} year The year to check 5822 * @return {Number} The number of days in the month 5823 */ 5824 ICAL.Time.daysInMonth = function icaltime_daysInMonth(month, year) { 5825 var _daysInMonth = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; 5826 var days = 30; 5827 5828 if (month < 1 || month > 12) return days; 5829 5830 days = _daysInMonth[month]; 5831 5832 if (month == 2) { 5833 days += ICAL.Time.isLeapYear(year); 5834 } 5835 5836 return days; 5837 }; 5838 5839 /** 5840 * Checks if the year is a leap year 5841 * 5842 * @param {Number} year The year to check 5843 * @return {Boolean} True, if the year is a leap year 5844 */ 5845 ICAL.Time.isLeapYear = function isLeapYear(year) { 5846 if (year <= 1752) { 5847 return ((year % 4) == 0); 5848 } else { 5849 return (((year % 4 == 0) && (year % 100 != 0)) || (year % 400 == 0)); 5850 } 5851 }; 5852 5853 /** 5854 * Create a new ICAL.Time from the day of year and year. The date is returned 5855 * in floating timezone. 5856 * 5857 * @param {Number} aDayOfYear The day of year 5858 * @param {Number} aYear The year to create the instance in 5859 * @return {ICAL.Time} The created instance with the calculated date 5860 */ 5861 ICAL.Time.fromDayOfYear = function icaltime_fromDayOfYear(aDayOfYear, aYear) { 5862 var year = aYear; 5863 var doy = aDayOfYear; 5864 var tt = new ICAL.Time(); 5865 tt.auto_normalize = false; 5866 var is_leap = (ICAL.Time.isLeapYear(year) ? 1 : 0); 5867 5868 if (doy < 1) { 5869 year--; 5870 is_leap = (ICAL.Time.isLeapYear(year) ? 1 : 0); 5871 doy += ICAL.Time.daysInYearPassedMonth[is_leap][12]; 5872 return ICAL.Time.fromDayOfYear(doy, year); 5873 } else if (doy > ICAL.Time.daysInYearPassedMonth[is_leap][12]) { 5874 is_leap = (ICAL.Time.isLeapYear(year) ? 1 : 0); 5875 doy -= ICAL.Time.daysInYearPassedMonth[is_leap][12]; 5876 year++; 5877 return ICAL.Time.fromDayOfYear(doy, year); 5878 } 5879 5880 tt.year = year; 5881 tt.isDate = true; 5882 5883 for (var month = 11; month >= 0; month--) { 5884 if (doy > ICAL.Time.daysInYearPassedMonth[is_leap][month]) { 5885 tt.month = month + 1; 5886 tt.day = doy - ICAL.Time.daysInYearPassedMonth[is_leap][month]; 5887 break; 5888 } 5889 } 5890 5891 tt.auto_normalize = true; 5892 return tt; 5893 }; 5894 5895 /** 5896 * Returns a new ICAL.Time instance from a date string, e.g 2015-01-02. 5897 * 5898 * @deprecated Use {@link ICAL.Time.fromDateString} instead 5899 * @param {String} str The string to create from 5900 * @return {ICAL.Time} The date/time instance 5901 */ 5902 ICAL.Time.fromStringv2 = function fromString(str) { 5903 return new ICAL.Time({ 5904 year: parseInt(str.substr(0, 4), 10), 5905 month: parseInt(str.substr(5, 2), 10), 5906 day: parseInt(str.substr(8, 2), 10), 5907 isDate: true 5908 }); 5909 }; 5910 5911 /** 5912 * Returns a new ICAL.Time instance from a date string, e.g 2015-01-02. 5913 * 5914 * @param {String} aValue The string to create from 5915 * @return {ICAL.Time} The date/time instance 5916 */ 5917 ICAL.Time.fromDateString = function(aValue) { 5918 // Dates should have no timezone. 5919 // Google likes to sometimes specify Z on dates 5920 // we specifically ignore that to avoid issues. 5921 5922 // YYYY-MM-DD 5923 // 2012-10-10 5924 return new ICAL.Time({ 5925 year: ICAL.helpers.strictParseInt(aValue.substr(0, 4)), 5926 month: ICAL.helpers.strictParseInt(aValue.substr(5, 2)), 5927 day: ICAL.helpers.strictParseInt(aValue.substr(8, 2)), 5928 isDate: true 5929 }); 5930 }; 5931 5932 /** 5933 * Returns a new ICAL.Time instance from a date-time string, e.g 5934 * 2015-01-02T03:04:05. If a property is specified, the timezone is set up 5935 * from the property's TZID parameter. 5936 * 5937 * @param {String} aValue The string to create from 5938 * @param {ICAL.Property=} prop The property the date belongs to 5939 * @return {ICAL.Time} The date/time instance 5940 */ 5941 ICAL.Time.fromDateTimeString = function(aValue, prop) { 5942 if (aValue.length < 19) { 5943 throw new Error( 5944 'invalid date-time value: "' + aValue + '"' 5945 ); 5946 } 5947 5948 var zone; 5949 5950 if (aValue[19] && aValue[19] === 'Z') { 5951 zone = 'Z'; 5952 } else if (prop) { 5953 zone = prop.getParameter('tzid'); 5954 } 5955 5956 // 2012-10-10T10:10:10(Z)? 5957 var time = new ICAL.Time({ 5958 year: ICAL.helpers.strictParseInt(aValue.substr(0, 4)), 5959 month: ICAL.helpers.strictParseInt(aValue.substr(5, 2)), 5960 day: ICAL.helpers.strictParseInt(aValue.substr(8, 2)), 5961 hour: ICAL.helpers.strictParseInt(aValue.substr(11, 2)), 5962 minute: ICAL.helpers.strictParseInt(aValue.substr(14, 2)), 5963 second: ICAL.helpers.strictParseInt(aValue.substr(17, 2)), 5964 timezone: zone 5965 }); 5966 5967 return time; 5968 }; 5969 5970 /** 5971 * Returns a new ICAL.Time instance from a date or date-time string, 5972 * 5973 * @param {String} aValue The string to create from 5974 * @param {ICAL.Property=} prop The property the date belongs to 5975 * @return {ICAL.Time} The date/time instance 5976 */ 5977 ICAL.Time.fromString = function fromString(aValue, aProperty) { 5978 if (aValue.length > 10) { 5979 return ICAL.Time.fromDateTimeString(aValue, aProperty); 5980 } else { 5981 return ICAL.Time.fromDateString(aValue); 5982 } 5983 }; 5984 5985 /** 5986 * Creates a new ICAL.Time instance from the given Javascript Date. 5987 * 5988 * @param {?Date} aDate The Javascript Date to read, or null to reset 5989 * @param {Boolean} useUTC If true, the UTC values of the date will be used 5990 */ 5991 ICAL.Time.fromJSDate = function fromJSDate(aDate, useUTC) { 5992 var tt = new ICAL.Time(); 5993 return tt.fromJSDate(aDate, useUTC); 5994 }; 5995 5996 /** 5997 * Creates a new ICAL.Time instance from the the passed data object. 5998 * 5999 * @param {Object} aData Time initialization 6000 * @param {Number=} aData.year The year for this date 6001 * @param {Number=} aData.month The month for this date 6002 * @param {Number=} aData.day The day for this date 6003 * @param {Number=} aData.hour The hour for this date 6004 * @param {Number=} aData.minute The minute for this date 6005 * @param {Number=} aData.second The second for this date 6006 * @param {Boolean=} aData.isDate If true, the instance represents a date 6007 * (as opposed to a date-time) 6008 * @param {ICAL.Timezone=} aZone Timezone this position occurs in 6009 */ 6010 ICAL.Time.fromData = function fromData(aData, aZone) { 6011 var t = new ICAL.Time(); 6012 return t.fromData(aData, aZone); 6013 }; 6014 6015 /** 6016 * Creates a new ICAL.Time instance from the current moment. 6017 * @return {ICAL.Time} 6018 */ 6019 ICAL.Time.now = function icaltime_now() { 6020 return ICAL.Time.fromJSDate(new Date(), false); 6021 }; 6022 6023 /** 6024 * Returns the date on which ISO week number 1 starts. 6025 * 6026 * @see ICAL.Time#weekNumber 6027 * @param {Number} aYear The year to search in 6028 * @param {ICAL.Time.weekDay=} aWeekStart The week start weekday, used for calculation. 6029 * @return {ICAL.Time} The date on which week number 1 starts 6030 */ 6031 ICAL.Time.weekOneStarts = function weekOneStarts(aYear, aWeekStart) { 6032 var t = ICAL.Time.fromData({ 6033 year: aYear, 6034 month: 1, 6035 day: 1, 6036 isDate: true 6037 }); 6038 6039 var dow = t.dayOfWeek(); 6040 var wkst = aWeekStart || ICAL.Time.DEFAULT_WEEK_START; 6041 if (dow > ICAL.Time.THURSDAY) { 6042 t.day += 7; 6043 } 6044 if (wkst > ICAL.Time.THURSDAY) { 6045 t.day -= 7; 6046 } 6047 6048 t.day -= dow - wkst; 6049 6050 return t; 6051 }; 6052 6053 /** 6054 * Get the dominical letter for the given year. Letters range from A - G for 6055 * common years, and AG to GF for leap years. 6056 * 6057 * @param {Number} yr The year to retrieve the letter for 6058 * @return {String} The dominical letter. 6059 */ 6060 ICAL.Time.getDominicalLetter = function(yr) { 6061 var LTRS = "GFEDCBA"; 6062 var dom = (yr + (yr / 4 | 0) + (yr / 400 | 0) - (yr / 100 | 0) - 1) % 7; 6063 var isLeap = ICAL.Time.isLeapYear(yr); 6064 if (isLeap) { 6065 return LTRS[(dom + 6) % 7] + LTRS[dom]; 6066 } else { 6067 return LTRS[dom]; 6068 } 6069 }; 6070 6071 /** 6072 * January 1st, 1970 as an ICAL.Time. 6073 * @type {ICAL.Time} 6074 * @constant 6075 * @instance 6076 */ 6077 ICAL.Time.epochTime = ICAL.Time.fromData({ 6078 year: 1970, 6079 month: 1, 6080 day: 1, 6081 hour: 0, 6082 minute: 0, 6083 second: 0, 6084 isDate: false, 6085 timezone: "Z" 6086 }); 6087 6088 ICAL.Time._cmp_attr = function _cmp_attr(a, b, attr) { 6089 if (a[attr] > b[attr]) return 1; 6090 if (a[attr] < b[attr]) return -1; 6091 return 0; 6092 }; 6093 6094 /** 6095 * The days that have passed in the year after a given month. The array has 6096 * two members, one being an array of passed days for non-leap years, the 6097 * other analog for leap years. 6098 * @example 6099 * var isLeapYear = ICAL.Time.isLeapYear(year); 6100 * var passedDays = ICAL.Time.daysInYearPassedMonth[isLeapYear][month]; 6101 * @type {Array.<Array.<Number>>} 6102 */ 6103 ICAL.Time.daysInYearPassedMonth = [ 6104 [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365], 6105 [0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366] 6106 ]; 6107 6108 /** 6109 * The weekday, 1 = SUNDAY, 7 = SATURDAY. Access via 6110 * ICAL.Time.MONDAY, ICAL.Time.TUESDAY, ... 6111 * 6112 * @typedef {Number} weekDay 6113 * @memberof ICAL.Time 6114 */ 6115 6116 ICAL.Time.SUNDAY = 1; 6117 ICAL.Time.MONDAY = 2; 6118 ICAL.Time.TUESDAY = 3; 6119 ICAL.Time.WEDNESDAY = 4; 6120 ICAL.Time.THURSDAY = 5; 6121 ICAL.Time.FRIDAY = 6; 6122 ICAL.Time.SATURDAY = 7; 6123 6124 /** 6125 * The default weekday for the WKST part. 6126 * @constant 6127 * @default ICAL.Time.MONDAY 6128 */ 6129 ICAL.Time.DEFAULT_WEEK_START = ICAL.Time.MONDAY; 6130})(); 6131/* This Source Code Form is subject to the terms of the Mozilla Public 6132 * License, v. 2.0. If a copy of the MPL was not distributed with this 6133 * file, You can obtain one at http://mozilla.org/MPL/2.0/. 6134 * Portions Copyright (C) Philipp Kewisch, 2015 */ 6135 6136 6137 6138(function() { 6139 6140 /** 6141 * Describes a vCard time, which has slight differences to the ICAL.Time. 6142 * Properties can be null if not specified, for example for dates with 6143 * reduced accuracy or truncation. 6144 * 6145 * Note that currently not all methods are correctly re-implemented for 6146 * VCardTime. For example, comparison will have undefined results when some 6147 * members are null. 6148 * 6149 * Also, normalization is not yet implemented for this class! 6150 * 6151 * @alias ICAL.VCardTime 6152 * @class 6153 * @extends {ICAL.Time} 6154 * @param {Object} data The data for the time instance 6155 * @param {Number=} data.year The year for this date 6156 * @param {Number=} data.month The month for this date 6157 * @param {Number=} data.day The day for this date 6158 * @param {Number=} data.hour The hour for this date 6159 * @param {Number=} data.minute The minute for this date 6160 * @param {Number=} data.second The second for this date 6161 * @param {ICAL.Timezone|ICAL.UtcOffset} zone The timezone to use 6162 * @param {String} icaltype The type for this date/time object 6163 */ 6164 ICAL.VCardTime = function(data, zone, icaltype) { 6165 this.wrappedJSObject = this; 6166 var time = this._time = Object.create(null); 6167 6168 time.year = null; 6169 time.month = null; 6170 time.day = null; 6171 time.hour = null; 6172 time.minute = null; 6173 time.second = null; 6174 6175 this.icaltype = icaltype || "date-and-or-time"; 6176 6177 this.fromData(data, zone); 6178 }; 6179 ICAL.helpers.inherits(ICAL.Time, ICAL.VCardTime, /** @lends ICAL.VCardTime */ { 6180 6181 /** 6182 * The class identifier. 6183 * @constant 6184 * @type {String} 6185 * @default "vcardtime" 6186 */ 6187 icalclass: "vcardtime", 6188 6189 /** 6190 * The type name, to be used in the jCal object. 6191 * @type {String} 6192 * @default "date-and-or-time" 6193 */ 6194 icaltype: "date-and-or-time", 6195 6196 /** 6197 * The timezone. This can either be floating, UTC, or an instance of 6198 * ICAL.UtcOffset. 6199 * @type {ICAL.Timezone|ICAL.UtcOFfset} 6200 */ 6201 zone: null, 6202 6203 /** 6204 * Returns a clone of the vcard date/time object. 6205 * 6206 * @return {ICAL.VCardTime} The cloned object 6207 */ 6208 clone: function() { 6209 return new ICAL.VCardTime(this._time, this.zone, this.icaltype); 6210 }, 6211 6212 _normalize: function() { 6213 return this; 6214 }, 6215 6216 /** 6217 * @inheritdoc 6218 */ 6219 utcOffset: function() { 6220 if (this.zone instanceof ICAL.UtcOffset) { 6221 return this.zone.toSeconds(); 6222 } else { 6223 return ICAL.Time.prototype.utcOffset.apply(this, arguments); 6224 } 6225 }, 6226 6227 /** 6228 * Returns an RFC 6350 compliant representation of this object. 6229 * 6230 * @return {String} vcard date/time string 6231 */ 6232 toICALString: function() { 6233 return ICAL.design.vcard.value[this.icaltype].toICAL(this.toString()); 6234 }, 6235 6236 /** 6237 * The string representation of this date/time, in jCard form 6238 * (including : and - separators). 6239 * @return {String} 6240 */ 6241 toString: function toString() { 6242 var p2 = ICAL.helpers.pad2; 6243 var y = this.year, m = this.month, d = this.day; 6244 var h = this.hour, mm = this.minute, s = this.second; 6245 6246 var hasYear = y !== null, hasMonth = m !== null, hasDay = d !== null; 6247 var hasHour = h !== null, hasMinute = mm !== null, hasSecond = s !== null; 6248 6249 var datepart = (hasYear ? p2(y) + (hasMonth || hasDay ? '-' : '') : (hasMonth || hasDay ? '--' : '')) + 6250 (hasMonth ? p2(m) : '') + 6251 (hasDay ? '-' + p2(d) : ''); 6252 var timepart = (hasHour ? p2(h) : '-') + (hasHour && hasMinute ? ':' : '') + 6253 (hasMinute ? p2(mm) : '') + (!hasHour && !hasMinute ? '-' : '') + 6254 (hasMinute && hasSecond ? ':' : '') + 6255 (hasSecond ? p2(s) : ''); 6256 6257 var zone; 6258 if (this.zone === ICAL.Timezone.utcTimezone) { 6259 zone = 'Z'; 6260 } else if (this.zone instanceof ICAL.UtcOffset) { 6261 zone = this.zone.toString(); 6262 } else if (this.zone === ICAL.Timezone.localTimezone) { 6263 zone = ''; 6264 } else if (this.zone instanceof ICAL.Timezone) { 6265 var offset = ICAL.UtcOffset.fromSeconds(this.zone.utcOffset(this)); 6266 zone = offset.toString(); 6267 } else { 6268 zone = ''; 6269 } 6270 6271 switch (this.icaltype) { 6272 case "time": 6273 return timepart + zone; 6274 case "date-and-or-time": 6275 case "date-time": 6276 return datepart + (timepart == '--' ? '' : 'T' + timepart + zone); 6277 case "date": 6278 return datepart; 6279 } 6280 return null; 6281 } 6282 }); 6283 6284 /** 6285 * Returns a new ICAL.VCardTime instance from a date and/or time string. 6286 * 6287 * @param {String} aValue The string to create from 6288 * @param {String} aIcalType The type for this instance, e.g. date-and-or-time 6289 * @return {ICAL.VCardTime} The date/time instance 6290 */ 6291 ICAL.VCardTime.fromDateAndOrTimeString = function(aValue, aIcalType) { 6292 function part(v, s, e) { 6293 return v ? ICAL.helpers.strictParseInt(v.substr(s, e)) : null; 6294 } 6295 var parts = aValue.split('T'); 6296 var dt = parts[0], tmz = parts[1]; 6297 var splitzone = tmz ? ICAL.design.vcard.value.time._splitZone(tmz) : []; 6298 var zone = splitzone[0], tm = splitzone[1]; 6299 6300 var stoi = ICAL.helpers.strictParseInt; 6301 var dtlen = dt ? dt.length : 0; 6302 var tmlen = tm ? tm.length : 0; 6303 6304 var hasDashDate = dt && dt[0] == '-' && dt[1] == '-'; 6305 var hasDashTime = tm && tm[0] == '-'; 6306 6307 var o = { 6308 year: hasDashDate ? null : part(dt, 0, 4), 6309 month: hasDashDate && (dtlen == 4 || dtlen == 7) ? part(dt, 2, 2) : dtlen == 7 ? part(dt, 5, 2) : dtlen == 10 ? part(dt, 5, 2) : null, 6310 day: dtlen == 5 ? part(dt, 3, 2) : dtlen == 7 && hasDashDate ? part(dt, 5, 2) : dtlen == 10 ? part(dt, 8, 2) : null, 6311 6312 hour: hasDashTime ? null : part(tm, 0, 2), 6313 minute: hasDashTime && tmlen == 3 ? part(tm, 1, 2) : tmlen > 4 ? hasDashTime ? part(tm, 1, 2) : part(tm, 3, 2) : null, 6314 second: tmlen == 4 ? part(tm, 2, 2) : tmlen == 6 ? part(tm, 4, 2) : tmlen == 8 ? part(tm, 6, 2) : null 6315 }; 6316 6317 if (zone == 'Z') { 6318 zone = ICAL.Timezone.utcTimezone; 6319 } else if (zone && zone[3] == ':') { 6320 zone = ICAL.UtcOffset.fromString(zone); 6321 } else { 6322 zone = null; 6323 } 6324 6325 return new ICAL.VCardTime(o, zone, aIcalType); 6326 }; 6327})(); 6328/* This Source Code Form is subject to the terms of the Mozilla Public 6329 * License, v. 2.0. If a copy of the MPL was not distributed with this 6330 * file, You can obtain one at http://mozilla.org/MPL/2.0/. 6331 * Portions Copyright (C) Philipp Kewisch, 2011-2015 */ 6332 6333 6334 6335(function() { 6336 var DOW_MAP = { 6337 SU: ICAL.Time.SUNDAY, 6338 MO: ICAL.Time.MONDAY, 6339 TU: ICAL.Time.TUESDAY, 6340 WE: ICAL.Time.WEDNESDAY, 6341 TH: ICAL.Time.THURSDAY, 6342 FR: ICAL.Time.FRIDAY, 6343 SA: ICAL.Time.SATURDAY 6344 }; 6345 6346 var REVERSE_DOW_MAP = {}; 6347 for (var key in DOW_MAP) { 6348 /* istanbul ignore else */ 6349 if (DOW_MAP.hasOwnProperty(key)) { 6350 REVERSE_DOW_MAP[DOW_MAP[key]] = key; 6351 } 6352 } 6353 6354 var COPY_PARTS = ["BYSECOND", "BYMINUTE", "BYHOUR", "BYDAY", 6355 "BYMONTHDAY", "BYYEARDAY", "BYWEEKNO", 6356 "BYMONTH", "BYSETPOS"]; 6357 6358 /** 6359 * @classdesc 6360 * This class represents the "recur" value type, with various calculation 6361 * and manipulation methods. 6362 * 6363 * @class 6364 * @alias ICAL.Recur 6365 * @param {Object} data An object with members of the recurrence 6366 * @param {ICAL.Recur.frequencyValues=} data.freq The frequency value 6367 * @param {Number=} data.interval The INTERVAL value 6368 * @param {ICAL.Time.weekDay=} data.wkst The week start value 6369 * @param {ICAL.Time=} data.until The end of the recurrence set 6370 * @param {Number=} data.count The number of occurrences 6371 * @param {Array.<Number>=} data.bysecond The seconds for the BYSECOND part 6372 * @param {Array.<Number>=} data.byminute The minutes for the BYMINUTE part 6373 * @param {Array.<Number>=} data.byhour The hours for the BYHOUR part 6374 * @param {Array.<String>=} data.byday The BYDAY values 6375 * @param {Array.<Number>=} data.bymonthday The days for the BYMONTHDAY part 6376 * @param {Array.<Number>=} data.byyearday The days for the BYYEARDAY part 6377 * @param {Array.<Number>=} data.byweekno The weeks for the BYWEEKNO part 6378 * @param {Array.<Number>=} data.bymonth The month for the BYMONTH part 6379 * @param {Array.<Number>=} data.bysetpos The positionals for the BYSETPOS part 6380 */ 6381 ICAL.Recur = function icalrecur(data) { 6382 this.wrappedJSObject = this; 6383 this.parts = {}; 6384 6385 if (data && typeof(data) === 'object') { 6386 this.fromData(data); 6387 } 6388 }; 6389 6390 ICAL.Recur.prototype = { 6391 /** 6392 * An object holding the BY-parts of the recurrence rule 6393 * @type {Object} 6394 */ 6395 parts: null, 6396 6397 /** 6398 * The interval value for the recurrence rule. 6399 * @type {Number} 6400 */ 6401 interval: 1, 6402 6403 /** 6404 * The week start day 6405 * 6406 * @type {ICAL.Time.weekDay} 6407 * @default ICAL.Time.MONDAY 6408 */ 6409 wkst: ICAL.Time.MONDAY, 6410 6411 /** 6412 * The end of the recurrence 6413 * @type {?ICAL.Time} 6414 */ 6415 until: null, 6416 6417 /** 6418 * The maximum number of occurrences 6419 * @type {?Number} 6420 */ 6421 count: null, 6422 6423 /** 6424 * The frequency value. 6425 * @type {ICAL.Recur.frequencyValues} 6426 */ 6427 freq: null, 6428 6429 /** 6430 * The class identifier. 6431 * @constant 6432 * @type {String} 6433 * @default "icalrecur" 6434 */ 6435 icalclass: "icalrecur", 6436 6437 /** 6438 * The type name, to be used in the jCal object. 6439 * @constant 6440 * @type {String} 6441 * @default "recur" 6442 */ 6443 icaltype: "recur", 6444 6445 /** 6446 * Create a new iterator for this recurrence rule. The passed start date 6447 * must be the start date of the event, not the start of the range to 6448 * search in. 6449 * 6450 * @example 6451 * var recur = comp.getFirstPropertyValue('rrule'); 6452 * var dtstart = comp.getFirstPropertyValue('dtstart'); 6453 * var iter = recur.iterator(dtstart); 6454 * for (var next = iter.next(); next; next = iter.next()) { 6455 * if (next.compare(rangeStart) < 0) { 6456 * continue; 6457 * } 6458 * console.log(next.toString()); 6459 * } 6460 * 6461 * @param {ICAL.Time} aStart The item's start date 6462 * @return {ICAL.RecurIterator} The recurrence iterator 6463 */ 6464 iterator: function(aStart) { 6465 return new ICAL.RecurIterator({ 6466 rule: this, 6467 dtstart: aStart 6468 }); 6469 }, 6470 6471 /** 6472 * Returns a clone of the recurrence object. 6473 * 6474 * @return {ICAL.Recur} The cloned object 6475 */ 6476 clone: function clone() { 6477 return new ICAL.Recur(this.toJSON()); 6478 }, 6479 6480 /** 6481 * Checks if the current rule is finite, i.e. has a count or until part. 6482 * 6483 * @return {Boolean} True, if the rule is finite 6484 */ 6485 isFinite: function isfinite() { 6486 return !!(this.count || this.until); 6487 }, 6488 6489 /** 6490 * Checks if the current rule has a count part, and not limited by an until 6491 * part. 6492 * 6493 * @return {Boolean} True, if the rule is by count 6494 */ 6495 isByCount: function isbycount() { 6496 return !!(this.count && !this.until); 6497 }, 6498 6499 /** 6500 * Adds a component (part) to the recurrence rule. This is not a component 6501 * in the sense of {@link ICAL.Component}, but a part of the recurrence 6502 * rule, i.e. BYMONTH. 6503 * 6504 * @param {String} aType The name of the component part 6505 * @param {Array|String} aValue The component value 6506 */ 6507 addComponent: function addPart(aType, aValue) { 6508 var ucname = aType.toUpperCase(); 6509 if (ucname in this.parts) { 6510 this.parts[ucname].push(aValue); 6511 } else { 6512 this.parts[ucname] = [aValue]; 6513 } 6514 }, 6515 6516 /** 6517 * Sets the component value for the given by-part. 6518 * 6519 * @param {String} aType The component part name 6520 * @param {Array} aValues The component values 6521 */ 6522 setComponent: function setComponent(aType, aValues) { 6523 this.parts[aType.toUpperCase()] = aValues.slice(); 6524 }, 6525 6526 /** 6527 * Gets (a copy) of the requested component value. 6528 * 6529 * @param {String} aType The component part name 6530 * @return {Array} The component part value 6531 */ 6532 getComponent: function getComponent(aType) { 6533 var ucname = aType.toUpperCase(); 6534 return (ucname in this.parts ? this.parts[ucname].slice() : []); 6535 }, 6536 6537 /** 6538 * Retrieves the next occurrence after the given recurrence id. See the 6539 * guide on {@tutorial terminology} for more details. 6540 * 6541 * NOTE: Currently, this method iterates all occurrences from the start 6542 * date. It should not be called in a loop for performance reasons. If you 6543 * would like to get more than one occurrence, you can iterate the 6544 * occurrences manually, see the example on the 6545 * {@link ICAL.Recur#iterator iterator} method. 6546 * 6547 * @param {ICAL.Time} aStartTime The start of the event series 6548 * @param {ICAL.Time} aRecurrenceId The date of the last occurrence 6549 * @return {ICAL.Time} The next occurrence after 6550 */ 6551 getNextOccurrence: function getNextOccurrence(aStartTime, aRecurrenceId) { 6552 var iter = this.iterator(aStartTime); 6553 var next, cdt; 6554 6555 do { 6556 next = iter.next(); 6557 } while (next && next.compare(aRecurrenceId) <= 0); 6558 6559 if (next && aRecurrenceId.zone) { 6560 next.zone = aRecurrenceId.zone; 6561 } 6562 6563 return next; 6564 }, 6565 6566 /** 6567 * Sets up the current instance using members from the passed data object. 6568 * 6569 * @param {Object} data An object with members of the recurrence 6570 * @param {ICAL.Recur.frequencyValues=} data.freq The frequency value 6571 * @param {Number=} data.interval The INTERVAL value 6572 * @param {ICAL.Time.weekDay=} data.wkst The week start value 6573 * @param {ICAL.Time=} data.until The end of the recurrence set 6574 * @param {Number=} data.count The number of occurrences 6575 * @param {Array.<Number>=} data.bysecond The seconds for the BYSECOND part 6576 * @param {Array.<Number>=} data.byminute The minutes for the BYMINUTE part 6577 * @param {Array.<Number>=} data.byhour The hours for the BYHOUR part 6578 * @param {Array.<String>=} data.byday The BYDAY values 6579 * @param {Array.<Number>=} data.bymonthday The days for the BYMONTHDAY part 6580 * @param {Array.<Number>=} data.byyearday The days for the BYYEARDAY part 6581 * @param {Array.<Number>=} data.byweekno The weeks for the BYWEEKNO part 6582 * @param {Array.<Number>=} data.bymonth The month for the BYMONTH part 6583 * @param {Array.<Number>=} data.bysetpos The positionals for the BYSETPOS part 6584 */ 6585 fromData: function(data) { 6586 for (var key in data) { 6587 var uckey = key.toUpperCase(); 6588 6589 if (uckey in partDesign) { 6590 if (Array.isArray(data[key])) { 6591 this.parts[uckey] = data[key]; 6592 } else { 6593 this.parts[uckey] = [data[key]]; 6594 } 6595 } else { 6596 this[key] = data[key]; 6597 } 6598 } 6599 6600 if (this.interval && typeof this.interval != "number") { 6601 optionDesign.INTERVAL(this.interval, this); 6602 } 6603 6604 if (this.wkst && typeof this.wkst != "number") { 6605 this.wkst = ICAL.Recur.icalDayToNumericDay(this.wkst); 6606 } 6607 6608 if (this.until && !(this.until instanceof ICAL.Time)) { 6609 this.until = ICAL.Time.fromString(this.until); 6610 } 6611 }, 6612 6613 /** 6614 * The jCal representation of this recurrence type. 6615 * @return {Object} 6616 */ 6617 toJSON: function() { 6618 var res = Object.create(null); 6619 res.freq = this.freq; 6620 6621 if (this.count) { 6622 res.count = this.count; 6623 } 6624 6625 if (this.interval > 1) { 6626 res.interval = this.interval; 6627 } 6628 6629 for (var k in this.parts) { 6630 /* istanbul ignore if */ 6631 if (!this.parts.hasOwnProperty(k)) { 6632 continue; 6633 } 6634 var kparts = this.parts[k]; 6635 if (Array.isArray(kparts) && kparts.length == 1) { 6636 res[k.toLowerCase()] = kparts[0]; 6637 } else { 6638 res[k.toLowerCase()] = ICAL.helpers.clone(this.parts[k]); 6639 } 6640 } 6641 6642 if (this.until) { 6643 res.until = this.until.toString(); 6644 } 6645 if ('wkst' in this && this.wkst !== ICAL.Time.DEFAULT_WEEK_START) { 6646 res.wkst = ICAL.Recur.numericDayToIcalDay(this.wkst); 6647 } 6648 return res; 6649 }, 6650 6651 /** 6652 * The string representation of this recurrence rule. 6653 * @return {String} 6654 */ 6655 toString: function icalrecur_toString() { 6656 // TODO retain order 6657 var str = "FREQ=" + this.freq; 6658 if (this.count) { 6659 str += ";COUNT=" + this.count; 6660 } 6661 if (this.interval > 1) { 6662 str += ";INTERVAL=" + this.interval; 6663 } 6664 for (var k in this.parts) { 6665 /* istanbul ignore else */ 6666 if (this.parts.hasOwnProperty(k)) { 6667 str += ";" + k + "=" + this.parts[k]; 6668 } 6669 } 6670 if (this.until) { 6671 str += ';UNTIL=' + this.until.toICALString(); 6672 } 6673 if ('wkst' in this && this.wkst !== ICAL.Time.DEFAULT_WEEK_START) { 6674 str += ';WKST=' + ICAL.Recur.numericDayToIcalDay(this.wkst); 6675 } 6676 return str; 6677 } 6678 }; 6679 6680 function parseNumericValue(type, min, max, value) { 6681 var result = value; 6682 6683 if (value[0] === '+') { 6684 result = value.substr(1); 6685 } 6686 6687 result = ICAL.helpers.strictParseInt(result); 6688 6689 if (min !== undefined && value < min) { 6690 throw new Error( 6691 type + ': invalid value "' + value + '" must be > ' + min 6692 ); 6693 } 6694 6695 if (max !== undefined && value > max) { 6696 throw new Error( 6697 type + ': invalid value "' + value + '" must be < ' + min 6698 ); 6699 } 6700 6701 return result; 6702 } 6703 6704 /** 6705 * Convert an ical representation of a day (SU, MO, etc..) 6706 * into a numeric value of that day. 6707 * 6708 * @param {String} string The iCalendar day name 6709 * @param {ICAL.Time.weekDay=} aWeekStart 6710 * The week start weekday, defaults to SUNDAY 6711 * @return {Number} Numeric value of given day 6712 */ 6713 ICAL.Recur.icalDayToNumericDay = function toNumericDay(string, aWeekStart) { 6714 //XXX: this is here so we can deal 6715 // with possibly invalid string values. 6716 var firstDow = aWeekStart || ICAL.Time.SUNDAY; 6717 return ((DOW_MAP[string] - firstDow + 7) % 7) + 1; 6718 }; 6719 6720 /** 6721 * Convert a numeric day value into its ical representation (SU, MO, etc..) 6722 * 6723 * @param {Number} num Numeric value of given day 6724 * @param {ICAL.Time.weekDay=} aWeekStart 6725 * The week start weekday, defaults to SUNDAY 6726 * @return {String} The ICAL day value, e.g SU,MO,... 6727 */ 6728 ICAL.Recur.numericDayToIcalDay = function toIcalDay(num, aWeekStart) { 6729 //XXX: this is here so we can deal with possibly invalid number values. 6730 // Also, this allows consistent mapping between day numbers and day 6731 // names for external users. 6732 var firstDow = aWeekStart || ICAL.Time.SUNDAY; 6733 var dow = (num + firstDow - ICAL.Time.SUNDAY); 6734 if (dow > 7) { 6735 dow -= 7; 6736 } 6737 return REVERSE_DOW_MAP[dow]; 6738 }; 6739 6740 var VALID_DAY_NAMES = /^(SU|MO|TU|WE|TH|FR|SA)$/; 6741 var VALID_BYDAY_PART = /^([+-])?(5[0-3]|[1-4][0-9]|[1-9])?(SU|MO|TU|WE|TH|FR|SA)$/; 6742 6743 /** 6744 * Possible frequency values for the FREQ part 6745 * (YEARLY, MONTHLY, WEEKLY, DAILY, HOURLY, MINUTELY, SECONDLY) 6746 * 6747 * @typedef {String} frequencyValues 6748 * @memberof ICAL.Recur 6749 */ 6750 6751 var ALLOWED_FREQ = ['SECONDLY', 'MINUTELY', 'HOURLY', 6752 'DAILY', 'WEEKLY', 'MONTHLY', 'YEARLY']; 6753 6754 var optionDesign = { 6755 FREQ: function(value, dict, fmtIcal) { 6756 // yes this is actually equal or faster then regex. 6757 // upside here is we can enumerate the valid values. 6758 if (ALLOWED_FREQ.indexOf(value) !== -1) { 6759 dict.freq = value; 6760 } else { 6761 throw new Error( 6762 'invalid frequency "' + value + '" expected: "' + 6763 ALLOWED_FREQ.join(', ') + '"' 6764 ); 6765 } 6766 }, 6767 6768 COUNT: function(value, dict, fmtIcal) { 6769 dict.count = ICAL.helpers.strictParseInt(value); 6770 }, 6771 6772 INTERVAL: function(value, dict, fmtIcal) { 6773 dict.interval = ICAL.helpers.strictParseInt(value); 6774 if (dict.interval < 1) { 6775 // 0 or negative values are not allowed, some engines seem to generate 6776 // it though. Assume 1 instead. 6777 dict.interval = 1; 6778 } 6779 }, 6780 6781 UNTIL: function(value, dict, fmtIcal) { 6782 if (value.length > 10) { 6783 dict.until = ICAL.design.icalendar.value['date-time'].fromICAL(value); 6784 } else { 6785 dict.until = ICAL.design.icalendar.value.date.fromICAL(value); 6786 } 6787 if (!fmtIcal) { 6788 dict.until = ICAL.Time.fromString(dict.until); 6789 } 6790 }, 6791 6792 WKST: function(value, dict, fmtIcal) { 6793 if (VALID_DAY_NAMES.test(value)) { 6794 dict.wkst = ICAL.Recur.icalDayToNumericDay(value); 6795 } else { 6796 throw new Error('invalid WKST value "' + value + '"'); 6797 } 6798 } 6799 }; 6800 6801 var partDesign = { 6802 BYSECOND: parseNumericValue.bind(this, 'BYSECOND', 0, 60), 6803 BYMINUTE: parseNumericValue.bind(this, 'BYMINUTE', 0, 59), 6804 BYHOUR: parseNumericValue.bind(this, 'BYHOUR', 0, 23), 6805 BYDAY: function(value) { 6806 if (VALID_BYDAY_PART.test(value)) { 6807 return value; 6808 } else { 6809 throw new Error('invalid BYDAY value "' + value + '"'); 6810 } 6811 }, 6812 BYMONTHDAY: parseNumericValue.bind(this, 'BYMONTHDAY', -31, 31), 6813 BYYEARDAY: parseNumericValue.bind(this, 'BYYEARDAY', -366, 366), 6814 BYWEEKNO: parseNumericValue.bind(this, 'BYWEEKNO', -53, 53), 6815 BYMONTH: parseNumericValue.bind(this, 'BYMONTH', 0, 12), 6816 BYSETPOS: parseNumericValue.bind(this, 'BYSETPOS', -366, 366) 6817 }; 6818 6819 6820 /** 6821 * Creates a new {@link ICAL.Recur} instance from the passed string. 6822 * 6823 * @param {String} string The string to parse 6824 * @return {ICAL.Recur} The created recurrence instance 6825 */ 6826 ICAL.Recur.fromString = function(string) { 6827 var data = ICAL.Recur._stringToData(string, false); 6828 return new ICAL.Recur(data); 6829 }; 6830 6831 /** 6832 * Creates a new {@link ICAL.Recur} instance using members from the passed 6833 * data object. 6834 * 6835 * @param {Object} aData An object with members of the recurrence 6836 * @param {ICAL.Recur.frequencyValues=} aData.freq The frequency value 6837 * @param {Number=} aData.interval The INTERVAL value 6838 * @param {ICAL.Time.weekDay=} aData.wkst The week start value 6839 * @param {ICAL.Time=} aData.until The end of the recurrence set 6840 * @param {Number=} aData.count The number of occurrences 6841 * @param {Array.<Number>=} aData.bysecond The seconds for the BYSECOND part 6842 * @param {Array.<Number>=} aData.byminute The minutes for the BYMINUTE part 6843 * @param {Array.<Number>=} aData.byhour The hours for the BYHOUR part 6844 * @param {Array.<String>=} aData.byday The BYDAY values 6845 * @param {Array.<Number>=} aData.bymonthday The days for the BYMONTHDAY part 6846 * @param {Array.<Number>=} aData.byyearday The days for the BYYEARDAY part 6847 * @param {Array.<Number>=} aData.byweekno The weeks for the BYWEEKNO part 6848 * @param {Array.<Number>=} aData.bymonth The month for the BYMONTH part 6849 * @param {Array.<Number>=} aData.bysetpos The positionals for the BYSETPOS part 6850 */ 6851 ICAL.Recur.fromData = function(aData) { 6852 return new ICAL.Recur(aData); 6853 }; 6854 6855 /** 6856 * Converts a recurrence string to a data object, suitable for the fromData 6857 * method. 6858 * 6859 * @param {String} string The string to parse 6860 * @param {Boolean} fmtIcal If true, the string is considered to be an 6861 * iCalendar string 6862 * @return {ICAL.Recur} The recurrence instance 6863 */ 6864 ICAL.Recur._stringToData = function(string, fmtIcal) { 6865 var dict = Object.create(null); 6866 6867 // split is slower in FF but fast enough. 6868 // v8 however this is faster then manual split? 6869 var values = string.split(';'); 6870 var len = values.length; 6871 6872 for (var i = 0; i < len; i++) { 6873 var parts = values[i].split('='); 6874 var ucname = parts[0].toUpperCase(); 6875 var lcname = parts[0].toLowerCase(); 6876 var name = (fmtIcal ? lcname : ucname); 6877 var value = parts[1]; 6878 6879 if (ucname in partDesign) { 6880 var partArr = value.split(','); 6881 var partArrIdx = 0; 6882 var partArrLen = partArr.length; 6883 6884 for (; partArrIdx < partArrLen; partArrIdx++) { 6885 partArr[partArrIdx] = partDesign[ucname](partArr[partArrIdx]); 6886 } 6887 dict[name] = (partArr.length == 1 ? partArr[0] : partArr); 6888 } else if (ucname in optionDesign) { 6889 optionDesign[ucname](value, dict, fmtIcal); 6890 } else { 6891 // Don't swallow unknown values. Just set them as they are. 6892 dict[lcname] = value; 6893 } 6894 } 6895 6896 return dict; 6897 }; 6898})(); 6899/* This Source Code Form is subject to the terms of the Mozilla Public 6900 * License, v. 2.0. If a copy of the MPL was not distributed with this 6901 * file, You can obtain one at http://mozilla.org/MPL/2.0/. 6902 * Portions Copyright (C) Philipp Kewisch, 2011-2015 */ 6903 6904 6905/** 6906 * This symbol is further described later on 6907 * @ignore 6908 */ 6909ICAL.RecurIterator = (function() { 6910 6911 /** 6912 * @classdesc 6913 * An iterator for a single recurrence rule. This class usually doesn't have 6914 * to be instanciated directly, the convenience method 6915 * {@link ICAL.Recur#iterator} can be used. 6916 * 6917 * @description 6918 * The options object may contain additional members when resuming iteration from a previous run 6919 * 6920 * @description 6921 * The options object may contain additional members when resuming iteration 6922 * from a previous run. 6923 * 6924 * @class 6925 * @alias ICAL.RecurIterator 6926 * @param {Object} options The iterator options 6927 * @param {ICAL.Recur} options.rule The rule to iterate. 6928 * @param {ICAL.Time} options.dtstart The start date of the event. 6929 * @param {Boolean=} options.initialized When true, assume that options are 6930 * from a previously constructed iterator. Initialization will not be 6931 * repeated. 6932 */ 6933 function icalrecur_iterator(options) { 6934 this.fromData(options); 6935 } 6936 6937 icalrecur_iterator.prototype = { 6938 6939 /** 6940 * True when iteration is finished. 6941 * @type {Boolean} 6942 */ 6943 completed: false, 6944 6945 /** 6946 * The rule that is being iterated 6947 * @type {ICAL.Recur} 6948 */ 6949 rule: null, 6950 6951 /** 6952 * The start date of the event being iterated. 6953 * @type {ICAL.Time} 6954 */ 6955 dtstart: null, 6956 6957 /** 6958 * The last occurrence that was returned from the 6959 * {@link ICAL.RecurIterator#next} method. 6960 * @type {ICAL.Time} 6961 */ 6962 last: null, 6963 6964 /** 6965 * The sequence number from the occurrence 6966 * @type {Number} 6967 */ 6968 occurrence_number: 0, 6969 6970 /** 6971 * The indices used for the {@link ICAL.RecurIterator#by_data} object. 6972 * @type {Object} 6973 * @private 6974 */ 6975 by_indices: null, 6976 6977 /** 6978 * If true, the iterator has already been initialized 6979 * @type {Boolean} 6980 * @private 6981 */ 6982 initialized: false, 6983 6984 /** 6985 * The initializd by-data. 6986 * @type {Object} 6987 * @private 6988 */ 6989 by_data: null, 6990 6991 /** 6992 * The expanded yeardays 6993 * @type {Array} 6994 * @private 6995 */ 6996 days: null, 6997 6998 /** 6999 * The index in the {@link ICAL.RecurIterator#days} array. 7000 * @type {Number} 7001 * @private 7002 */ 7003 days_index: 0, 7004 7005 /** 7006 * Initialize the recurrence iterator from the passed data object. This 7007 * method is usually not called directly, you can initialize the iterator 7008 * through the constructor. 7009 * 7010 * @param {Object} options The iterator options 7011 * @param {ICAL.Recur} options.rule The rule to iterate. 7012 * @param {ICAL.Time} options.dtstart The start date of the event. 7013 * @param {Boolean=} options.initialized When true, assume that options are 7014 * from a previously constructed iterator. Initialization will not be 7015 * repeated. 7016 */ 7017 fromData: function(options) { 7018 this.rule = ICAL.helpers.formatClassType(options.rule, ICAL.Recur); 7019 7020 if (!this.rule) { 7021 throw new Error('iterator requires a (ICAL.Recur) rule'); 7022 } 7023 7024 this.dtstart = ICAL.helpers.formatClassType(options.dtstart, ICAL.Time); 7025 7026 if (!this.dtstart) { 7027 throw new Error('iterator requires a (ICAL.Time) dtstart'); 7028 } 7029 7030 if (options.by_data) { 7031 this.by_data = options.by_data; 7032 } else { 7033 this.by_data = ICAL.helpers.clone(this.rule.parts, true); 7034 } 7035 7036 if (options.occurrence_number) 7037 this.occurrence_number = options.occurrence_number; 7038 7039 this.days = options.days || []; 7040 if (options.last) { 7041 this.last = ICAL.helpers.formatClassType(options.last, ICAL.Time); 7042 } 7043 7044 this.by_indices = options.by_indices; 7045 7046 if (!this.by_indices) { 7047 this.by_indices = { 7048 "BYSECOND": 0, 7049 "BYMINUTE": 0, 7050 "BYHOUR": 0, 7051 "BYDAY": 0, 7052 "BYMONTH": 0, 7053 "BYWEEKNO": 0, 7054 "BYMONTHDAY": 0 7055 }; 7056 } 7057 7058 this.initialized = options.initialized || false; 7059 7060 if (!this.initialized) { 7061 this.init(); 7062 } 7063 }, 7064 7065 /** 7066 * Intialize the iterator 7067 * @private 7068 */ 7069 init: function icalrecur_iterator_init() { 7070 this.initialized = true; 7071 this.last = this.dtstart.clone(); 7072 var parts = this.by_data; 7073 7074 if ("BYDAY" in parts) { 7075 // libical does this earlier when the rule is loaded, but we postpone to 7076 // now so we can preserve the original order. 7077 this.sort_byday_rules(parts.BYDAY); 7078 } 7079 7080 // If the BYYEARDAY appares, no other date rule part may appear 7081 if ("BYYEARDAY" in parts) { 7082 if ("BYMONTH" in parts || "BYWEEKNO" in parts || 7083 "BYMONTHDAY" in parts || "BYDAY" in parts) { 7084 throw new Error("Invalid BYYEARDAY rule"); 7085 } 7086 } 7087 7088 // BYWEEKNO and BYMONTHDAY rule parts may not both appear 7089 if ("BYWEEKNO" in parts && "BYMONTHDAY" in parts) { 7090 throw new Error("BYWEEKNO does not fit to BYMONTHDAY"); 7091 } 7092 7093 // For MONTHLY recurrences (FREQ=MONTHLY) neither BYYEARDAY nor 7094 // BYWEEKNO may appear. 7095 if (this.rule.freq == "MONTHLY" && 7096 ("BYYEARDAY" in parts || "BYWEEKNO" in parts)) { 7097 throw new Error("For MONTHLY recurrences neither BYYEARDAY nor BYWEEKNO may appear"); 7098 } 7099 7100 // For WEEKLY recurrences (FREQ=WEEKLY) neither BYMONTHDAY nor 7101 // BYYEARDAY may appear. 7102 if (this.rule.freq == "WEEKLY" && 7103 ("BYYEARDAY" in parts || "BYMONTHDAY" in parts)) { 7104 throw new Error("For WEEKLY recurrences neither BYMONTHDAY nor BYYEARDAY may appear"); 7105 } 7106 7107 // BYYEARDAY may only appear in YEARLY rules 7108 if (this.rule.freq != "YEARLY" && "BYYEARDAY" in parts) { 7109 throw new Error("BYYEARDAY may only appear in YEARLY rules"); 7110 } 7111 7112 this.last.second = this.setup_defaults("BYSECOND", "SECONDLY", this.dtstart.second); 7113 this.last.minute = this.setup_defaults("BYMINUTE", "MINUTELY", this.dtstart.minute); 7114 this.last.hour = this.setup_defaults("BYHOUR", "HOURLY", this.dtstart.hour); 7115 this.last.day = this.setup_defaults("BYMONTHDAY", "DAILY", this.dtstart.day); 7116 this.last.month = this.setup_defaults("BYMONTH", "MONTHLY", this.dtstart.month); 7117 7118 if (this.rule.freq == "WEEKLY") { 7119 if ("BYDAY" in parts) { 7120 var bydayParts = this.ruleDayOfWeek(parts.BYDAY[0], this.rule.wkst); 7121 var pos = bydayParts[0]; 7122 var dow = bydayParts[1]; 7123 var wkdy = dow - this.last.dayOfWeek(this.rule.wkst); 7124 if ((this.last.dayOfWeek(this.rule.wkst) < dow && wkdy >= 0) || wkdy < 0) { 7125 // Initial time is after first day of BYDAY data 7126 this.last.day += wkdy; 7127 } 7128 } else { 7129 var dayName = ICAL.Recur.numericDayToIcalDay(this.dtstart.dayOfWeek()); 7130 parts.BYDAY = [dayName]; 7131 } 7132 } 7133 7134 if (this.rule.freq == "YEARLY") { 7135 for (;;) { 7136 this.expand_year_days(this.last.year); 7137 if (this.days.length > 0) { 7138 break; 7139 } 7140 this.increment_year(this.rule.interval); 7141 } 7142 7143 this._nextByYearDay(); 7144 } 7145 7146 if (this.rule.freq == "MONTHLY" && this.has_by_data("BYDAY")) { 7147 var tempLast = null; 7148 var initLast = this.last.clone(); 7149 var daysInMonth = ICAL.Time.daysInMonth(this.last.month, this.last.year); 7150 7151 // Check every weekday in BYDAY with relative dow and pos. 7152 for (var i in this.by_data.BYDAY) { 7153 /* istanbul ignore if */ 7154 if (!this.by_data.BYDAY.hasOwnProperty(i)) { 7155 continue; 7156 } 7157 this.last = initLast.clone(); 7158 var bydayParts = this.ruleDayOfWeek(this.by_data.BYDAY[i]); 7159 var pos = bydayParts[0]; 7160 var dow = bydayParts[1]; 7161 var dayOfMonth = this.last.nthWeekDay(dow, pos); 7162 7163 // If |pos| >= 6, the byday is invalid for a monthly rule. 7164 if (pos >= 6 || pos <= -6) { 7165 throw new Error("Malformed values in BYDAY part"); 7166 } 7167 7168 // If a Byday with pos=+/-5 is not in the current month it 7169 // must be searched in the next months. 7170 if (dayOfMonth > daysInMonth || dayOfMonth <= 0) { 7171 // Skip if we have already found a "last" in this month. 7172 if (tempLast && tempLast.month == initLast.month) { 7173 continue; 7174 } 7175 while (dayOfMonth > daysInMonth || dayOfMonth <= 0) { 7176 this.increment_month(); 7177 daysInMonth = ICAL.Time.daysInMonth(this.last.month, this.last.year); 7178 dayOfMonth = this.last.nthWeekDay(dow, pos); 7179 } 7180 } 7181 7182 this.last.day = dayOfMonth; 7183 if (!tempLast || this.last.compare(tempLast) < 0) { 7184 tempLast = this.last.clone(); 7185 } 7186 } 7187 this.last = tempLast.clone(); 7188 7189 //XXX: This feels like a hack, but we need to initialize 7190 // the BYMONTHDAY case correctly and byDayAndMonthDay handles 7191 // this case. It accepts a special flag which will avoid incrementing 7192 // the initial value without the flag days that match the start time 7193 // would be missed. 7194 if (this.has_by_data('BYMONTHDAY')) { 7195 this._byDayAndMonthDay(true); 7196 } 7197 7198 if (this.last.day > daysInMonth || this.last.day == 0) { 7199 throw new Error("Malformed values in BYDAY part"); 7200 } 7201 7202 } else if (this.has_by_data("BYMONTHDAY")) { 7203 if (this.last.day < 0) { 7204 var daysInMonth = ICAL.Time.daysInMonth(this.last.month, this.last.year); 7205 this.last.day = daysInMonth + this.last.day + 1; 7206 } 7207 } 7208 7209 }, 7210 7211 /** 7212 * Retrieve the next occurrence from the iterator. 7213 * @return {ICAL.Time} 7214 */ 7215 next: function icalrecur_iterator_next() { 7216 var before = (this.last ? this.last.clone() : null); 7217 7218 if ((this.rule.count && this.occurrence_number >= this.rule.count) || 7219 (this.rule.until && this.last.compare(this.rule.until) > 0)) { 7220 7221 //XXX: right now this is just a flag and has no impact 7222 // we can simplify the above case to check for completed later. 7223 this.completed = true; 7224 7225 return null; 7226 } 7227 7228 if (this.occurrence_number == 0 && this.last.compare(this.dtstart) >= 0) { 7229 // First of all, give the instance that was initialized 7230 this.occurrence_number++; 7231 return this.last; 7232 } 7233 7234 7235 var valid; 7236 do { 7237 valid = 1; 7238 7239 switch (this.rule.freq) { 7240 case "SECONDLY": 7241 this.next_second(); 7242 break; 7243 case "MINUTELY": 7244 this.next_minute(); 7245 break; 7246 case "HOURLY": 7247 this.next_hour(); 7248 break; 7249 case "DAILY": 7250 this.next_day(); 7251 break; 7252 case "WEEKLY": 7253 this.next_week(); 7254 break; 7255 case "MONTHLY": 7256 valid = this.next_month(); 7257 break; 7258 case "YEARLY": 7259 this.next_year(); 7260 break; 7261 7262 default: 7263 return null; 7264 } 7265 } while (!this.check_contracting_rules() || 7266 this.last.compare(this.dtstart) < 0 || 7267 !valid); 7268 7269 // TODO is this valid? 7270 if (this.last.compare(before) == 0) { 7271 throw new Error("Same occurrence found twice, protecting " + 7272 "you from death by recursion"); 7273 } 7274 7275 if (this.rule.until && this.last.compare(this.rule.until) > 0) { 7276 this.completed = true; 7277 return null; 7278 } else { 7279 this.occurrence_number++; 7280 return this.last; 7281 } 7282 }, 7283 7284 next_second: function next_second() { 7285 return this.next_generic("BYSECOND", "SECONDLY", "second", "minute"); 7286 }, 7287 7288 increment_second: function increment_second(inc) { 7289 return this.increment_generic(inc, "second", 60, "minute"); 7290 }, 7291 7292 next_minute: function next_minute() { 7293 return this.next_generic("BYMINUTE", "MINUTELY", 7294 "minute", "hour", "next_second"); 7295 }, 7296 7297 increment_minute: function increment_minute(inc) { 7298 return this.increment_generic(inc, "minute", 60, "hour"); 7299 }, 7300 7301 next_hour: function next_hour() { 7302 return this.next_generic("BYHOUR", "HOURLY", "hour", 7303 "monthday", "next_minute"); 7304 }, 7305 7306 increment_hour: function increment_hour(inc) { 7307 this.increment_generic(inc, "hour", 24, "monthday"); 7308 }, 7309 7310 next_day: function next_day() { 7311 var has_by_day = ("BYDAY" in this.by_data); 7312 var this_freq = (this.rule.freq == "DAILY"); 7313 7314 if (this.next_hour() == 0) { 7315 return 0; 7316 } 7317 7318 if (this_freq) { 7319 this.increment_monthday(this.rule.interval); 7320 } else { 7321 this.increment_monthday(1); 7322 } 7323 7324 return 0; 7325 }, 7326 7327 next_week: function next_week() { 7328 var end_of_data = 0; 7329 7330 if (this.next_weekday_by_week() == 0) { 7331 return end_of_data; 7332 } 7333 7334 if (this.has_by_data("BYWEEKNO")) { 7335 var idx = ++this.by_indices.BYWEEKNO; 7336 7337 if (this.by_indices.BYWEEKNO == this.by_data.BYWEEKNO.length) { 7338 this.by_indices.BYWEEKNO = 0; 7339 end_of_data = 1; 7340 } 7341 7342 // HACK should be first month of the year 7343 this.last.month = 1; 7344 this.last.day = 1; 7345 7346 var week_no = this.by_data.BYWEEKNO[this.by_indices.BYWEEKNO]; 7347 7348 this.last.day += 7 * week_no; 7349 7350 if (end_of_data) { 7351 this.increment_year(1); 7352 } 7353 } else { 7354 // Jump to the next week 7355 this.increment_monthday(7 * this.rule.interval); 7356 } 7357 7358 return end_of_data; 7359 }, 7360 7361 /** 7362 * Normalize each by day rule for a given year/month. 7363 * Takes into account ordering and negative rules 7364 * 7365 * @private 7366 * @param {Number} year Current year. 7367 * @param {Number} month Current month. 7368 * @param {Array} rules Array of rules. 7369 * 7370 * @return {Array} sorted and normalized rules. 7371 * Negative rules will be expanded to their 7372 * correct positive values for easier processing. 7373 */ 7374 normalizeByMonthDayRules: function(year, month, rules) { 7375 var daysInMonth = ICAL.Time.daysInMonth(month, year); 7376 7377 // XXX: This is probably bad for performance to allocate 7378 // a new array for each month we scan, if possible 7379 // we should try to optimize this... 7380 var newRules = []; 7381 7382 var ruleIdx = 0; 7383 var len = rules.length; 7384 var rule; 7385 7386 for (; ruleIdx < len; ruleIdx++) { 7387 rule = rules[ruleIdx]; 7388 7389 // if this rule falls outside of given 7390 // month discard it. 7391 if (Math.abs(rule) > daysInMonth) { 7392 continue; 7393 } 7394 7395 // negative case 7396 if (rule < 0) { 7397 // we add (not subtract its a negative number) 7398 // one from the rule because 1 === last day of month 7399 rule = daysInMonth + (rule + 1); 7400 } else if (rule === 0) { 7401 // skip zero its invalid. 7402 continue; 7403 } 7404 7405 // only add unique items... 7406 if (newRules.indexOf(rule) === -1) { 7407 newRules.push(rule); 7408 } 7409 7410 } 7411 7412 // unique and sort 7413 return newRules.sort(function(a, b) { return a - b; }); 7414 }, 7415 7416 /** 7417 * NOTES: 7418 * We are given a list of dates in the month (BYMONTHDAY) (23, etc..) 7419 * Also we are given a list of days (BYDAY) (MO, 2SU, etc..) when 7420 * both conditions match a given date (this.last.day) iteration stops. 7421 * 7422 * @private 7423 * @param {Boolean=} isInit When given true will not increment the 7424 * current day (this.last). 7425 */ 7426 _byDayAndMonthDay: function(isInit) { 7427 var byMonthDay; // setup in initMonth 7428 var byDay = this.by_data.BYDAY; 7429 7430 var date; 7431 var dateIdx = 0; 7432 var dateLen; // setup in initMonth 7433 var dayLen = byDay.length; 7434 7435 // we are not valid by default 7436 var dataIsValid = 0; 7437 7438 var daysInMonth; 7439 var self = this; 7440 // we need a copy of this, because a DateTime gets normalized 7441 // automatically if the day is out of range. At some points we 7442 // set the last day to 0 to start counting. 7443 var lastDay = this.last.day; 7444 7445 function initMonth() { 7446 daysInMonth = ICAL.Time.daysInMonth( 7447 self.last.month, self.last.year 7448 ); 7449 7450 byMonthDay = self.normalizeByMonthDayRules( 7451 self.last.year, 7452 self.last.month, 7453 self.by_data.BYMONTHDAY 7454 ); 7455 7456 dateLen = byMonthDay.length; 7457 7458 // For the case of more than one occurrence in one month 7459 // we have to be sure to start searching after the last 7460 // found date or at the last BYMONTHDAY, unless we are 7461 // initializing the iterator because in this case we have 7462 // to consider the last found date too. 7463 while (byMonthDay[dateIdx] <= lastDay && 7464 !(isInit && byMonthDay[dateIdx] == lastDay) && 7465 dateIdx < dateLen - 1) { 7466 dateIdx++; 7467 } 7468 } 7469 7470 function nextMonth() { 7471 // since the day is incremented at the start 7472 // of the loop below, we need to start at 0 7473 lastDay = 0; 7474 self.increment_month(); 7475 dateIdx = 0; 7476 initMonth(); 7477 } 7478 7479 initMonth(); 7480 7481 // should come after initMonth 7482 if (isInit) { 7483 lastDay -= 1; 7484 } 7485 7486 // Use a counter to avoid an infinite loop with malformed rules. 7487 // Stop checking after 4 years so we consider also a leap year. 7488 var monthsCounter = 48; 7489 7490 while (!dataIsValid && monthsCounter) { 7491 monthsCounter--; 7492 // increment the current date. This is really 7493 // important otherwise we may fall into the infinite 7494 // loop trap. The initial date takes care of the case 7495 // where the current date is the date we are looking 7496 // for. 7497 date = lastDay + 1; 7498 7499 if (date > daysInMonth) { 7500 nextMonth(); 7501 continue; 7502 } 7503 7504 // find next date 7505 var next = byMonthDay[dateIdx++]; 7506 7507 // this logic is dependant on the BYMONTHDAYS 7508 // being in order (which is done by #normalizeByMonthDayRules) 7509 if (next >= date) { 7510 // if the next month day is in the future jump to it. 7511 lastDay = next; 7512 } else { 7513 // in this case the 'next' monthday has past 7514 // we must move to the month. 7515 nextMonth(); 7516 continue; 7517 } 7518 7519 // Now we can loop through the day rules to see 7520 // if one matches the current month date. 7521 for (var dayIdx = 0; dayIdx < dayLen; dayIdx++) { 7522 var parts = this.ruleDayOfWeek(byDay[dayIdx]); 7523 var pos = parts[0]; 7524 var dow = parts[1]; 7525 7526 this.last.day = lastDay; 7527 if (this.last.isNthWeekDay(dow, pos)) { 7528 // when we find the valid one we can mark 7529 // the conditions as met and break the loop. 7530 // (Because we have this condition above 7531 // it will also break the parent loop). 7532 dataIsValid = 1; 7533 break; 7534 } 7535 } 7536 7537 // Its completely possible that the combination 7538 // cannot be matched in the current month. 7539 // When we reach the end of possible combinations 7540 // in the current month we iterate to the next one. 7541 // since dateIdx is incremented right after getting 7542 // "next", we don't need dateLen -1 here. 7543 if (!dataIsValid && dateIdx === dateLen) { 7544 nextMonth(); 7545 continue; 7546 } 7547 } 7548 7549 if (monthsCounter <= 0) { 7550 // Checked 4 years without finding a Byday that matches 7551 // a Bymonthday. Maybe the rule is not correct. 7552 throw new Error("Malformed values in BYDAY combined with BYMONTHDAY parts"); 7553 } 7554 7555 7556 return dataIsValid; 7557 }, 7558 7559 next_month: function next_month() { 7560 var this_freq = (this.rule.freq == "MONTHLY"); 7561 var data_valid = 1; 7562 7563 if (this.next_hour() == 0) { 7564 return data_valid; 7565 } 7566 7567 if (this.has_by_data("BYDAY") && this.has_by_data("BYMONTHDAY")) { 7568 data_valid = this._byDayAndMonthDay(); 7569 } else if (this.has_by_data("BYDAY")) { 7570 var daysInMonth = ICAL.Time.daysInMonth(this.last.month, this.last.year); 7571 var setpos = 0; 7572 var setpos_total = 0; 7573 7574 if (this.has_by_data("BYSETPOS")) { 7575 var last_day = this.last.day; 7576 for (var day = 1; day <= daysInMonth; day++) { 7577 this.last.day = day; 7578 if (this.is_day_in_byday(this.last)) { 7579 setpos_total++; 7580 if (day <= last_day) { 7581 setpos++; 7582 } 7583 } 7584 } 7585 this.last.day = last_day; 7586 } 7587 7588 data_valid = 0; 7589 for (var day = this.last.day + 1; day <= daysInMonth; day++) { 7590 this.last.day = day; 7591 7592 if (this.is_day_in_byday(this.last)) { 7593 if (!this.has_by_data("BYSETPOS") || 7594 this.check_set_position(++setpos) || 7595 this.check_set_position(setpos - setpos_total - 1)) { 7596 7597 data_valid = 1; 7598 break; 7599 } 7600 } 7601 } 7602 7603 if (day > daysInMonth) { 7604 this.last.day = 1; 7605 this.increment_month(); 7606 7607 if (this.is_day_in_byday(this.last)) { 7608 if (!this.has_by_data("BYSETPOS") || this.check_set_position(1)) { 7609 data_valid = 1; 7610 } 7611 } else { 7612 data_valid = 0; 7613 } 7614 } 7615 } else if (this.has_by_data("BYMONTHDAY")) { 7616 this.by_indices.BYMONTHDAY++; 7617 7618 if (this.by_indices.BYMONTHDAY >= this.by_data.BYMONTHDAY.length) { 7619 this.by_indices.BYMONTHDAY = 0; 7620 this.increment_month(); 7621 } 7622 7623 var daysInMonth = ICAL.Time.daysInMonth(this.last.month, this.last.year); 7624 var day = this.by_data.BYMONTHDAY[this.by_indices.BYMONTHDAY]; 7625 7626 if (day < 0) { 7627 day = daysInMonth + day + 1; 7628 } 7629 7630 if (day > daysInMonth) { 7631 this.last.day = 1; 7632 data_valid = this.is_day_in_byday(this.last); 7633 } else { 7634 this.last.day = day; 7635 } 7636 7637 } else { 7638 this.increment_month(); 7639 var daysInMonth = ICAL.Time.daysInMonth(this.last.month, this.last.year); 7640 if (this.by_data.BYMONTHDAY[0] > daysInMonth) { 7641 data_valid = 0; 7642 } else { 7643 this.last.day = this.by_data.BYMONTHDAY[0]; 7644 } 7645 } 7646 7647 return data_valid; 7648 }, 7649 7650 next_weekday_by_week: function next_weekday_by_week() { 7651 var end_of_data = 0; 7652 7653 if (this.next_hour() == 0) { 7654 return end_of_data; 7655 } 7656 7657 if (!this.has_by_data("BYDAY")) { 7658 return 1; 7659 } 7660 7661 for (;;) { 7662 var tt = new ICAL.Time(); 7663 this.by_indices.BYDAY++; 7664 7665 if (this.by_indices.BYDAY == Object.keys(this.by_data.BYDAY).length) { 7666 this.by_indices.BYDAY = 0; 7667 end_of_data = 1; 7668 } 7669 7670 var coded_day = this.by_data.BYDAY[this.by_indices.BYDAY]; 7671 var parts = this.ruleDayOfWeek(coded_day); 7672 var dow = parts[1]; 7673 7674 dow -= this.rule.wkst; 7675 7676 if (dow < 0) { 7677 dow += 7; 7678 } 7679 7680 tt.year = this.last.year; 7681 tt.month = this.last.month; 7682 tt.day = this.last.day; 7683 7684 var startOfWeek = tt.startDoyWeek(this.rule.wkst); 7685 7686 if (dow + startOfWeek < 1) { 7687 // The selected date is in the previous year 7688 if (!end_of_data) { 7689 continue; 7690 } 7691 } 7692 7693 var next = ICAL.Time.fromDayOfYear(startOfWeek + dow, 7694 this.last.year); 7695 7696 /** 7697 * The normalization horrors below are due to 7698 * the fact that when the year/month/day changes 7699 * it can effect the other operations that come after. 7700 */ 7701 this.last.year = next.year; 7702 this.last.month = next.month; 7703 this.last.day = next.day; 7704 7705 return end_of_data; 7706 } 7707 }, 7708 7709 next_year: function next_year() { 7710 7711 if (this.next_hour() == 0) { 7712 return 0; 7713 } 7714 7715 if (++this.days_index == this.days.length) { 7716 this.days_index = 0; 7717 do { 7718 this.increment_year(this.rule.interval); 7719 this.expand_year_days(this.last.year); 7720 } while (this.days.length == 0); 7721 } 7722 7723 this._nextByYearDay(); 7724 7725 return 1; 7726 }, 7727 7728 _nextByYearDay: function _nextByYearDay() { 7729 var doy = this.days[this.days_index]; 7730 var year = this.last.year; 7731 if (doy < 1) { 7732 // Time.fromDayOfYear(doy, year) indexes relative to the 7733 // start of the given year. That is different from the 7734 // semantics of BYYEARDAY where negative indexes are an 7735 // offset from the end of the given year. 7736 doy += 1; 7737 year += 1; 7738 } 7739 var next = ICAL.Time.fromDayOfYear(doy, year); 7740 this.last.day = next.day; 7741 this.last.month = next.month; 7742 }, 7743 7744 /** 7745 * @param dow (eg: '1TU', '-1MO') 7746 * @param {ICAL.Time.weekDay=} aWeekStart The week start weekday 7747 * @return [pos, numericDow] (eg: [1, 3]) numericDow is relative to aWeekStart 7748 */ 7749 ruleDayOfWeek: function ruleDayOfWeek(dow, aWeekStart) { 7750 var matches = dow.match(/([+-]?[0-9])?(MO|TU|WE|TH|FR|SA|SU)/); 7751 if (matches) { 7752 var pos = parseInt(matches[1] || 0, 10); 7753 dow = ICAL.Recur.icalDayToNumericDay(matches[2], aWeekStart); 7754 return [pos, dow]; 7755 } else { 7756 return [0, 0]; 7757 } 7758 }, 7759 7760 next_generic: function next_generic(aRuleType, aInterval, aDateAttr, 7761 aFollowingAttr, aPreviousIncr) { 7762 var has_by_rule = (aRuleType in this.by_data); 7763 var this_freq = (this.rule.freq == aInterval); 7764 var end_of_data = 0; 7765 7766 if (aPreviousIncr && this[aPreviousIncr]() == 0) { 7767 return end_of_data; 7768 } 7769 7770 if (has_by_rule) { 7771 this.by_indices[aRuleType]++; 7772 var idx = this.by_indices[aRuleType]; 7773 var dta = this.by_data[aRuleType]; 7774 7775 if (this.by_indices[aRuleType] == dta.length) { 7776 this.by_indices[aRuleType] = 0; 7777 end_of_data = 1; 7778 } 7779 this.last[aDateAttr] = dta[this.by_indices[aRuleType]]; 7780 } else if (this_freq) { 7781 this["increment_" + aDateAttr](this.rule.interval); 7782 } 7783 7784 if (has_by_rule && end_of_data && this_freq) { 7785 this["increment_" + aFollowingAttr](1); 7786 } 7787 7788 return end_of_data; 7789 }, 7790 7791 increment_monthday: function increment_monthday(inc) { 7792 for (var i = 0; i < inc; i++) { 7793 var daysInMonth = ICAL.Time.daysInMonth(this.last.month, this.last.year); 7794 this.last.day++; 7795 7796 if (this.last.day > daysInMonth) { 7797 this.last.day -= daysInMonth; 7798 this.increment_month(); 7799 } 7800 } 7801 }, 7802 7803 increment_month: function increment_month() { 7804 this.last.day = 1; 7805 if (this.has_by_data("BYMONTH")) { 7806 this.by_indices.BYMONTH++; 7807 7808 if (this.by_indices.BYMONTH == this.by_data.BYMONTH.length) { 7809 this.by_indices.BYMONTH = 0; 7810 this.increment_year(1); 7811 } 7812 7813 this.last.month = this.by_data.BYMONTH[this.by_indices.BYMONTH]; 7814 } else { 7815 if (this.rule.freq == "MONTHLY") { 7816 this.last.month += this.rule.interval; 7817 } else { 7818 this.last.month++; 7819 } 7820 7821 this.last.month--; 7822 var years = ICAL.helpers.trunc(this.last.month / 12); 7823 this.last.month %= 12; 7824 this.last.month++; 7825 7826 if (years != 0) { 7827 this.increment_year(years); 7828 } 7829 } 7830 }, 7831 7832 increment_year: function increment_year(inc) { 7833 this.last.year += inc; 7834 }, 7835 7836 increment_generic: function increment_generic(inc, aDateAttr, 7837 aFactor, aNextIncrement) { 7838 this.last[aDateAttr] += inc; 7839 var nextunit = ICAL.helpers.trunc(this.last[aDateAttr] / aFactor); 7840 this.last[aDateAttr] %= aFactor; 7841 if (nextunit != 0) { 7842 this["increment_" + aNextIncrement](nextunit); 7843 } 7844 }, 7845 7846 has_by_data: function has_by_data(aRuleType) { 7847 return (aRuleType in this.rule.parts); 7848 }, 7849 7850 expand_year_days: function expand_year_days(aYear) { 7851 var t = new ICAL.Time(); 7852 this.days = []; 7853 7854 // We need our own copy with a few keys set 7855 var parts = {}; 7856 var rules = ["BYDAY", "BYWEEKNO", "BYMONTHDAY", "BYMONTH", "BYYEARDAY"]; 7857 for (var p in rules) { 7858 /* istanbul ignore else */ 7859 if (rules.hasOwnProperty(p)) { 7860 var part = rules[p]; 7861 if (part in this.rule.parts) { 7862 parts[part] = this.rule.parts[part]; 7863 } 7864 } 7865 } 7866 7867 if ("BYMONTH" in parts && "BYWEEKNO" in parts) { 7868 var valid = 1; 7869 var validWeeks = {}; 7870 t.year = aYear; 7871 t.isDate = true; 7872 7873 for (var monthIdx = 0; monthIdx < this.by_data.BYMONTH.length; monthIdx++) { 7874 var month = this.by_data.BYMONTH[monthIdx]; 7875 t.month = month; 7876 t.day = 1; 7877 var first_week = t.weekNumber(this.rule.wkst); 7878 t.day = ICAL.Time.daysInMonth(month, aYear); 7879 var last_week = t.weekNumber(this.rule.wkst); 7880 for (monthIdx = first_week; monthIdx < last_week; monthIdx++) { 7881 validWeeks[monthIdx] = 1; 7882 } 7883 } 7884 7885 for (var weekIdx = 0; weekIdx < this.by_data.BYWEEKNO.length && valid; weekIdx++) { 7886 var weekno = this.by_data.BYWEEKNO[weekIdx]; 7887 if (weekno < 52) { 7888 valid &= validWeeks[weekIdx]; 7889 } else { 7890 valid = 0; 7891 } 7892 } 7893 7894 if (valid) { 7895 delete parts.BYMONTH; 7896 } else { 7897 delete parts.BYWEEKNO; 7898 } 7899 } 7900 7901 var partCount = Object.keys(parts).length; 7902 7903 if (partCount == 0) { 7904 var t1 = this.dtstart.clone(); 7905 t1.year = this.last.year; 7906 this.days.push(t1.dayOfYear()); 7907 } else if (partCount == 1 && "BYMONTH" in parts) { 7908 for (var monthkey in this.by_data.BYMONTH) { 7909 /* istanbul ignore if */ 7910 if (!this.by_data.BYMONTH.hasOwnProperty(monthkey)) { 7911 continue; 7912 } 7913 var t2 = this.dtstart.clone(); 7914 t2.year = aYear; 7915 t2.month = this.by_data.BYMONTH[monthkey]; 7916 t2.isDate = true; 7917 this.days.push(t2.dayOfYear()); 7918 } 7919 } else if (partCount == 1 && "BYMONTHDAY" in parts) { 7920 for (var monthdaykey in this.by_data.BYMONTHDAY) { 7921 /* istanbul ignore if */ 7922 if (!this.by_data.BYMONTHDAY.hasOwnProperty(monthdaykey)) { 7923 continue; 7924 } 7925 var t3 = this.dtstart.clone(); 7926 var day_ = this.by_data.BYMONTHDAY[monthdaykey]; 7927 if (day_ < 0) { 7928 var daysInMonth = ICAL.Time.daysInMonth(t3.month, aYear); 7929 day_ = day_ + daysInMonth + 1; 7930 } 7931 t3.day = day_; 7932 t3.year = aYear; 7933 t3.isDate = true; 7934 this.days.push(t3.dayOfYear()); 7935 } 7936 } else if (partCount == 2 && 7937 "BYMONTHDAY" in parts && 7938 "BYMONTH" in parts) { 7939 for (var monthkey in this.by_data.BYMONTH) { 7940 /* istanbul ignore if */ 7941 if (!this.by_data.BYMONTH.hasOwnProperty(monthkey)) { 7942 continue; 7943 } 7944 var month_ = this.by_data.BYMONTH[monthkey]; 7945 var daysInMonth = ICAL.Time.daysInMonth(month_, aYear); 7946 for (var monthdaykey in this.by_data.BYMONTHDAY) { 7947 /* istanbul ignore if */ 7948 if (!this.by_data.BYMONTHDAY.hasOwnProperty(monthdaykey)) { 7949 continue; 7950 } 7951 var day_ = this.by_data.BYMONTHDAY[monthdaykey]; 7952 if (day_ < 0) { 7953 day_ = day_ + daysInMonth + 1; 7954 } 7955 t.day = day_; 7956 t.month = month_; 7957 t.year = aYear; 7958 t.isDate = true; 7959 7960 this.days.push(t.dayOfYear()); 7961 } 7962 } 7963 } else if (partCount == 1 && "BYWEEKNO" in parts) { 7964 // TODO unimplemented in libical 7965 } else if (partCount == 2 && 7966 "BYWEEKNO" in parts && 7967 "BYMONTHDAY" in parts) { 7968 // TODO unimplemented in libical 7969 } else if (partCount == 1 && "BYDAY" in parts) { 7970 this.days = this.days.concat(this.expand_by_day(aYear)); 7971 } else if (partCount == 2 && "BYDAY" in parts && "BYMONTH" in parts) { 7972 for (var monthkey in this.by_data.BYMONTH) { 7973 /* istanbul ignore if */ 7974 if (!this.by_data.BYMONTH.hasOwnProperty(monthkey)) { 7975 continue; 7976 } 7977 var month = this.by_data.BYMONTH[monthkey]; 7978 var daysInMonth = ICAL.Time.daysInMonth(month, aYear); 7979 7980 t.year = aYear; 7981 t.month = this.by_data.BYMONTH[monthkey]; 7982 t.day = 1; 7983 t.isDate = true; 7984 7985 var first_dow = t.dayOfWeek(); 7986 var doy_offset = t.dayOfYear() - 1; 7987 7988 t.day = daysInMonth; 7989 var last_dow = t.dayOfWeek(); 7990 7991 if (this.has_by_data("BYSETPOS")) { 7992 var set_pos_counter = 0; 7993 var by_month_day = []; 7994 for (var day = 1; day <= daysInMonth; day++) { 7995 t.day = day; 7996 if (this.is_day_in_byday(t)) { 7997 by_month_day.push(day); 7998 } 7999 } 8000 8001 for (var spIndex = 0; spIndex < by_month_day.length; spIndex++) { 8002 if (this.check_set_position(spIndex + 1) || 8003 this.check_set_position(spIndex - by_month_day.length)) { 8004 this.days.push(doy_offset + by_month_day[spIndex]); 8005 } 8006 } 8007 } else { 8008 for (var daycodedkey in this.by_data.BYDAY) { 8009 /* istanbul ignore if */ 8010 if (!this.by_data.BYDAY.hasOwnProperty(daycodedkey)) { 8011 continue; 8012 } 8013 var coded_day = this.by_data.BYDAY[daycodedkey]; 8014 var bydayParts = this.ruleDayOfWeek(coded_day); 8015 var pos = bydayParts[0]; 8016 var dow = bydayParts[1]; 8017 var month_day; 8018 8019 var first_matching_day = ((dow + 7 - first_dow) % 7) + 1; 8020 var last_matching_day = daysInMonth - ((last_dow + 7 - dow) % 7); 8021 8022 if (pos == 0) { 8023 for (var day = first_matching_day; day <= daysInMonth; day += 7) { 8024 this.days.push(doy_offset + day); 8025 } 8026 } else if (pos > 0) { 8027 month_day = first_matching_day + (pos - 1) * 7; 8028 8029 if (month_day <= daysInMonth) { 8030 this.days.push(doy_offset + month_day); 8031 } 8032 } else { 8033 month_day = last_matching_day + (pos + 1) * 7; 8034 8035 if (month_day > 0) { 8036 this.days.push(doy_offset + month_day); 8037 } 8038 } 8039 } 8040 } 8041 } 8042 // Return dates in order of occurrence (1,2,3,...) instead 8043 // of by groups of weekdays (1,8,15,...,2,9,16,...). 8044 this.days.sort(function(a, b) { return a - b; }); // Comparator function allows to sort numbers. 8045 } else if (partCount == 2 && "BYDAY" in parts && "BYMONTHDAY" in parts) { 8046 var expandedDays = this.expand_by_day(aYear); 8047 8048 for (var daykey in expandedDays) { 8049 /* istanbul ignore if */ 8050 if (!expandedDays.hasOwnProperty(daykey)) { 8051 continue; 8052 } 8053 var day = expandedDays[daykey]; 8054 var tt = ICAL.Time.fromDayOfYear(day, aYear); 8055 if (this.by_data.BYMONTHDAY.indexOf(tt.day) >= 0) { 8056 this.days.push(day); 8057 } 8058 } 8059 } else if (partCount == 3 && 8060 "BYDAY" in parts && 8061 "BYMONTHDAY" in parts && 8062 "BYMONTH" in parts) { 8063 var expandedDays = this.expand_by_day(aYear); 8064 8065 for (var daykey in expandedDays) { 8066 /* istanbul ignore if */ 8067 if (!expandedDays.hasOwnProperty(daykey)) { 8068 continue; 8069 } 8070 var day = expandedDays[daykey]; 8071 var tt = ICAL.Time.fromDayOfYear(day, aYear); 8072 8073 if (this.by_data.BYMONTH.indexOf(tt.month) >= 0 && 8074 this.by_data.BYMONTHDAY.indexOf(tt.day) >= 0) { 8075 this.days.push(day); 8076 } 8077 } 8078 } else if (partCount == 2 && "BYDAY" in parts && "BYWEEKNO" in parts) { 8079 var expandedDays = this.expand_by_day(aYear); 8080 8081 for (var daykey in expandedDays) { 8082 /* istanbul ignore if */ 8083 if (!expandedDays.hasOwnProperty(daykey)) { 8084 continue; 8085 } 8086 var day = expandedDays[daykey]; 8087 var tt = ICAL.Time.fromDayOfYear(day, aYear); 8088 var weekno = tt.weekNumber(this.rule.wkst); 8089 8090 if (this.by_data.BYWEEKNO.indexOf(weekno)) { 8091 this.days.push(day); 8092 } 8093 } 8094 } else if (partCount == 3 && 8095 "BYDAY" in parts && 8096 "BYWEEKNO" in parts && 8097 "BYMONTHDAY" in parts) { 8098 // TODO unimplemted in libical 8099 } else if (partCount == 1 && "BYYEARDAY" in parts) { 8100 this.days = this.days.concat(this.by_data.BYYEARDAY); 8101 } else { 8102 this.days = []; 8103 } 8104 return 0; 8105 }, 8106 8107 expand_by_day: function expand_by_day(aYear) { 8108 8109 var days_list = []; 8110 var tmp = this.last.clone(); 8111 8112 tmp.year = aYear; 8113 tmp.month = 1; 8114 tmp.day = 1; 8115 tmp.isDate = true; 8116 8117 var start_dow = tmp.dayOfWeek(); 8118 8119 tmp.month = 12; 8120 tmp.day = 31; 8121 tmp.isDate = true; 8122 8123 var end_dow = tmp.dayOfWeek(); 8124 var end_year_day = tmp.dayOfYear(); 8125 8126 for (var daykey in this.by_data.BYDAY) { 8127 /* istanbul ignore if */ 8128 if (!this.by_data.BYDAY.hasOwnProperty(daykey)) { 8129 continue; 8130 } 8131 var day = this.by_data.BYDAY[daykey]; 8132 var parts = this.ruleDayOfWeek(day); 8133 var pos = parts[0]; 8134 var dow = parts[1]; 8135 8136 if (pos == 0) { 8137 var tmp_start_doy = ((dow + 7 - start_dow) % 7) + 1; 8138 8139 for (var doy = tmp_start_doy; doy <= end_year_day; doy += 7) { 8140 days_list.push(doy); 8141 } 8142 8143 } else if (pos > 0) { 8144 var first; 8145 if (dow >= start_dow) { 8146 first = dow - start_dow + 1; 8147 } else { 8148 first = dow - start_dow + 8; 8149 } 8150 8151 days_list.push(first + (pos - 1) * 7); 8152 } else { 8153 var last; 8154 pos = -pos; 8155 8156 if (dow <= end_dow) { 8157 last = end_year_day - end_dow + dow; 8158 } else { 8159 last = end_year_day - end_dow + dow - 7; 8160 } 8161 8162 days_list.push(last - (pos - 1) * 7); 8163 } 8164 } 8165 return days_list; 8166 }, 8167 8168 is_day_in_byday: function is_day_in_byday(tt) { 8169 for (var daykey in this.by_data.BYDAY) { 8170 /* istanbul ignore if */ 8171 if (!this.by_data.BYDAY.hasOwnProperty(daykey)) { 8172 continue; 8173 } 8174 var day = this.by_data.BYDAY[daykey]; 8175 var parts = this.ruleDayOfWeek(day); 8176 var pos = parts[0]; 8177 var dow = parts[1]; 8178 var this_dow = tt.dayOfWeek(); 8179 8180 if ((pos == 0 && dow == this_dow) || 8181 (tt.nthWeekDay(dow, pos) == tt.day)) { 8182 return 1; 8183 } 8184 } 8185 8186 return 0; 8187 }, 8188 8189 /** 8190 * Checks if given value is in BYSETPOS. 8191 * 8192 * @private 8193 * @param {Numeric} aPos position to check for. 8194 * @return {Boolean} false unless BYSETPOS rules exist 8195 * and the given value is present in rules. 8196 */ 8197 check_set_position: function check_set_position(aPos) { 8198 if (this.has_by_data('BYSETPOS')) { 8199 var idx = this.by_data.BYSETPOS.indexOf(aPos); 8200 // negative numbers are not false-y 8201 return idx !== -1; 8202 } 8203 return false; 8204 }, 8205 8206 sort_byday_rules: function icalrecur_sort_byday_rules(aRules) { 8207 for (var i = 0; i < aRules.length; i++) { 8208 for (var j = 0; j < i; j++) { 8209 var one = this.ruleDayOfWeek(aRules[j], this.rule.wkst)[1]; 8210 var two = this.ruleDayOfWeek(aRules[i], this.rule.wkst)[1]; 8211 8212 if (one > two) { 8213 var tmp = aRules[i]; 8214 aRules[i] = aRules[j]; 8215 aRules[j] = tmp; 8216 } 8217 } 8218 } 8219 }, 8220 8221 check_contract_restriction: function check_contract_restriction(aRuleType, v) { 8222 var indexMapValue = icalrecur_iterator._indexMap[aRuleType]; 8223 var ruleMapValue = icalrecur_iterator._expandMap[this.rule.freq][indexMapValue]; 8224 var pass = false; 8225 8226 if (aRuleType in this.by_data && 8227 ruleMapValue == icalrecur_iterator.CONTRACT) { 8228 8229 var ruleType = this.by_data[aRuleType]; 8230 8231 for (var bydatakey in ruleType) { 8232 /* istanbul ignore else */ 8233 if (ruleType.hasOwnProperty(bydatakey)) { 8234 if (ruleType[bydatakey] == v) { 8235 pass = true; 8236 break; 8237 } 8238 } 8239 } 8240 } else { 8241 // Not a contracting byrule or has no data, test passes 8242 pass = true; 8243 } 8244 return pass; 8245 }, 8246 8247 check_contracting_rules: function check_contracting_rules() { 8248 var dow = this.last.dayOfWeek(); 8249 var weekNo = this.last.weekNumber(this.rule.wkst); 8250 var doy = this.last.dayOfYear(); 8251 8252 return (this.check_contract_restriction("BYSECOND", this.last.second) && 8253 this.check_contract_restriction("BYMINUTE", this.last.minute) && 8254 this.check_contract_restriction("BYHOUR", this.last.hour) && 8255 this.check_contract_restriction("BYDAY", ICAL.Recur.numericDayToIcalDay(dow)) && 8256 this.check_contract_restriction("BYWEEKNO", weekNo) && 8257 this.check_contract_restriction("BYMONTHDAY", this.last.day) && 8258 this.check_contract_restriction("BYMONTH", this.last.month) && 8259 this.check_contract_restriction("BYYEARDAY", doy)); 8260 }, 8261 8262 setup_defaults: function setup_defaults(aRuleType, req, deftime) { 8263 var indexMapValue = icalrecur_iterator._indexMap[aRuleType]; 8264 var ruleMapValue = icalrecur_iterator._expandMap[this.rule.freq][indexMapValue]; 8265 8266 if (ruleMapValue != icalrecur_iterator.CONTRACT) { 8267 if (!(aRuleType in this.by_data)) { 8268 this.by_data[aRuleType] = [deftime]; 8269 } 8270 if (this.rule.freq != req) { 8271 return this.by_data[aRuleType][0]; 8272 } 8273 } 8274 return deftime; 8275 }, 8276 8277 /** 8278 * Convert iterator into a serialize-able object. Will preserve current 8279 * iteration sequence to ensure the seamless continuation of the recurrence 8280 * rule. 8281 * @return {Object} 8282 */ 8283 toJSON: function() { 8284 var result = Object.create(null); 8285 8286 result.initialized = this.initialized; 8287 result.rule = this.rule.toJSON(); 8288 result.dtstart = this.dtstart.toJSON(); 8289 result.by_data = this.by_data; 8290 result.days = this.days; 8291 result.last = this.last.toJSON(); 8292 result.by_indices = this.by_indices; 8293 result.occurrence_number = this.occurrence_number; 8294 8295 return result; 8296 } 8297 }; 8298 8299 icalrecur_iterator._indexMap = { 8300 "BYSECOND": 0, 8301 "BYMINUTE": 1, 8302 "BYHOUR": 2, 8303 "BYDAY": 3, 8304 "BYMONTHDAY": 4, 8305 "BYYEARDAY": 5, 8306 "BYWEEKNO": 6, 8307 "BYMONTH": 7, 8308 "BYSETPOS": 8 8309 }; 8310 8311 icalrecur_iterator._expandMap = { 8312 "SECONDLY": [1, 1, 1, 1, 1, 1, 1, 1], 8313 "MINUTELY": [2, 1, 1, 1, 1, 1, 1, 1], 8314 "HOURLY": [2, 2, 1, 1, 1, 1, 1, 1], 8315 "DAILY": [2, 2, 2, 1, 1, 1, 1, 1], 8316 "WEEKLY": [2, 2, 2, 2, 3, 3, 1, 1], 8317 "MONTHLY": [2, 2, 2, 2, 2, 3, 3, 1], 8318 "YEARLY": [2, 2, 2, 2, 2, 2, 2, 2] 8319 }; 8320 icalrecur_iterator.UNKNOWN = 0; 8321 icalrecur_iterator.CONTRACT = 1; 8322 icalrecur_iterator.EXPAND = 2; 8323 icalrecur_iterator.ILLEGAL = 3; 8324 8325 return icalrecur_iterator; 8326 8327}()); 8328/* This Source Code Form is subject to the terms of the Mozilla Public 8329 * License, v. 2.0. If a copy of the MPL was not distributed with this 8330 * file, You can obtain one at http://mozilla.org/MPL/2.0/. 8331 * Portions Copyright (C) Philipp Kewisch, 2011-2015 */ 8332 8333 8334/** 8335 * This symbol is further described later on 8336 * @ignore 8337 */ 8338ICAL.RecurExpansion = (function() { 8339 function formatTime(item) { 8340 return ICAL.helpers.formatClassType(item, ICAL.Time); 8341 } 8342 8343 function compareTime(a, b) { 8344 return a.compare(b); 8345 } 8346 8347 function isRecurringComponent(comp) { 8348 return comp.hasProperty('rdate') || 8349 comp.hasProperty('rrule') || 8350 comp.hasProperty('recurrence-id'); 8351 } 8352 8353 /** 8354 * @classdesc 8355 * Primary class for expanding recurring rules. Can take multiple rrules, 8356 * rdates, exdate(s) and iterate (in order) over each next occurrence. 8357 * 8358 * Once initialized this class can also be serialized saved and continue 8359 * iteration from the last point. 8360 * 8361 * NOTE: it is intended that this class is to be used 8362 * with ICAL.Event which handles recurrence exceptions. 8363 * 8364 * @example 8365 * // assuming event is a parsed ical component 8366 * var event; 8367 * 8368 * var expand = new ICAL.RecurExpansion({ 8369 * component: event, 8370 * dtstart: event.getFirstPropertyValue('dtstart') 8371 * }); 8372 * 8373 * // remember there are infinite rules 8374 * // so its a good idea to limit the scope 8375 * // of the iterations then resume later on. 8376 * 8377 * // next is always an ICAL.Time or null 8378 * var next; 8379 * 8380 * while (someCondition && (next = expand.next())) { 8381 * // do something with next 8382 * } 8383 * 8384 * // save instance for later 8385 * var json = JSON.stringify(expand); 8386 * 8387 * //... 8388 * 8389 * // NOTE: if the component's properties have 8390 * // changed you will need to rebuild the 8391 * // class and start over. This only works 8392 * // when the component's recurrence info is the same. 8393 * var expand = new ICAL.RecurExpansion(JSON.parse(json)); 8394 * 8395 * @description 8396 * The options object can be filled with the specified initial values. It can 8397 * also contain additional members, as a result of serializing a previous 8398 * expansion state, as shown in the example. 8399 * 8400 * @class 8401 * @alias ICAL.RecurExpansion 8402 * @param {Object} options 8403 * Recurrence expansion options 8404 * @param {ICAL.Time} options.dtstart 8405 * Start time of the event 8406 * @param {ICAL.Component=} options.component 8407 * Component for expansion, required if not resuming. 8408 */ 8409 function RecurExpansion(options) { 8410 this.ruleDates = []; 8411 this.exDates = []; 8412 this.fromData(options); 8413 } 8414 8415 RecurExpansion.prototype = { 8416 /** 8417 * True when iteration is fully completed. 8418 * @type {Boolean} 8419 */ 8420 complete: false, 8421 8422 /** 8423 * Array of rrule iterators. 8424 * 8425 * @type {ICAL.RecurIterator[]} 8426 * @private 8427 */ 8428 ruleIterators: null, 8429 8430 /** 8431 * Array of rdate instances. 8432 * 8433 * @type {ICAL.Time[]} 8434 * @private 8435 */ 8436 ruleDates: null, 8437 8438 /** 8439 * Array of exdate instances. 8440 * 8441 * @type {ICAL.Time[]} 8442 * @private 8443 */ 8444 exDates: null, 8445 8446 /** 8447 * Current position in ruleDates array. 8448 * @type {Number} 8449 * @private 8450 */ 8451 ruleDateInc: 0, 8452 8453 /** 8454 * Current position in exDates array 8455 * @type {Number} 8456 * @private 8457 */ 8458 exDateInc: 0, 8459 8460 /** 8461 * Current negative date. 8462 * 8463 * @type {ICAL.Time} 8464 * @private 8465 */ 8466 exDate: null, 8467 8468 /** 8469 * Current additional date. 8470 * 8471 * @type {ICAL.Time} 8472 * @private 8473 */ 8474 ruleDate: null, 8475 8476 /** 8477 * Start date of recurring rules. 8478 * 8479 * @type {ICAL.Time} 8480 */ 8481 dtstart: null, 8482 8483 /** 8484 * Last expanded time 8485 * 8486 * @type {ICAL.Time} 8487 */ 8488 last: null, 8489 8490 /** 8491 * Initialize the recurrence expansion from the data object. The options 8492 * object may also contain additional members, see the 8493 * {@link ICAL.RecurExpansion constructor} for more details. 8494 * 8495 * @param {Object} options 8496 * Recurrence expansion options 8497 * @param {ICAL.Time} options.dtstart 8498 * Start time of the event 8499 * @param {ICAL.Component=} options.component 8500 * Component for expansion, required if not resuming. 8501 */ 8502 fromData: function(options) { 8503 var start = ICAL.helpers.formatClassType(options.dtstart, ICAL.Time); 8504 8505 if (!start) { 8506 throw new Error('.dtstart (ICAL.Time) must be given'); 8507 } else { 8508 this.dtstart = start; 8509 } 8510 8511 if (options.component) { 8512 this._init(options.component); 8513 } else { 8514 this.last = formatTime(options.last) || start.clone(); 8515 8516 if (!options.ruleIterators) { 8517 throw new Error('.ruleIterators or .component must be given'); 8518 } 8519 8520 this.ruleIterators = options.ruleIterators.map(function(item) { 8521 return ICAL.helpers.formatClassType(item, ICAL.RecurIterator); 8522 }); 8523 8524 this.ruleDateInc = options.ruleDateInc; 8525 this.exDateInc = options.exDateInc; 8526 8527 if (options.ruleDates) { 8528 this.ruleDates = options.ruleDates.map(formatTime); 8529 this.ruleDate = this.ruleDates[this.ruleDateInc]; 8530 } 8531 8532 if (options.exDates) { 8533 this.exDates = options.exDates.map(formatTime); 8534 this.exDate = this.exDates[this.exDateInc]; 8535 } 8536 8537 if (typeof(options.complete) !== 'undefined') { 8538 this.complete = options.complete; 8539 } 8540 } 8541 }, 8542 8543 /** 8544 * Retrieve the next occurrence in the series. 8545 * @return {ICAL.Time} 8546 */ 8547 next: function() { 8548 var iter; 8549 var ruleOfDay; 8550 var next; 8551 var compare; 8552 8553 var maxTries = 500; 8554 var currentTry = 0; 8555 8556 while (true) { 8557 if (currentTry++ > maxTries) { 8558 throw new Error( 8559 'max tries have occured, rule may be impossible to forfill.' 8560 ); 8561 } 8562 8563 next = this.ruleDate; 8564 iter = this._nextRecurrenceIter(this.last); 8565 8566 // no more matches 8567 // because we increment the rule day or rule 8568 // _after_ we choose a value this should be 8569 // the only spot where we need to worry about the 8570 // end of events. 8571 if (!next && !iter) { 8572 // there are no more iterators or rdates 8573 this.complete = true; 8574 break; 8575 } 8576 8577 // no next rule day or recurrence rule is first. 8578 if (!next || (iter && next.compare(iter.last) > 0)) { 8579 // must be cloned, recur will reuse the time element. 8580 next = iter.last.clone(); 8581 // move to next so we can continue 8582 iter.next(); 8583 } 8584 8585 // if the ruleDate is still next increment it. 8586 if (this.ruleDate === next) { 8587 this._nextRuleDay(); 8588 } 8589 8590 this.last = next; 8591 8592 // check the negative rules 8593 if (this.exDate) { 8594 compare = this.exDate.compare(this.last); 8595 8596 if (compare < 0) { 8597 this._nextExDay(); 8598 } 8599 8600 // if the current rule is excluded skip it. 8601 if (compare === 0) { 8602 this._nextExDay(); 8603 continue; 8604 } 8605 } 8606 8607 //XXX: The spec states that after we resolve the final 8608 // list of dates we execute exdate this seems somewhat counter 8609 // intuitive to what I have seen most servers do so for now 8610 // I exclude based on the original date not the one that may 8611 // have been modified by the exception. 8612 return this.last; 8613 } 8614 }, 8615 8616 /** 8617 * Converts object into a serialize-able format. This format can be passed 8618 * back into the expansion to resume iteration. 8619 * @return {Object} 8620 */ 8621 toJSON: function() { 8622 function toJSON(item) { 8623 return item.toJSON(); 8624 } 8625 8626 var result = Object.create(null); 8627 result.ruleIterators = this.ruleIterators.map(toJSON); 8628 8629 if (this.ruleDates) { 8630 result.ruleDates = this.ruleDates.map(toJSON); 8631 } 8632 8633 if (this.exDates) { 8634 result.exDates = this.exDates.map(toJSON); 8635 } 8636 8637 result.ruleDateInc = this.ruleDateInc; 8638 result.exDateInc = this.exDateInc; 8639 result.last = this.last.toJSON(); 8640 result.dtstart = this.dtstart.toJSON(); 8641 result.complete = this.complete; 8642 8643 return result; 8644 }, 8645 8646 /** 8647 * Extract all dates from the properties in the given component. The 8648 * properties will be filtered by the property name. 8649 * 8650 * @private 8651 * @param {ICAL.Component} component The component to search in 8652 * @param {String} propertyName The property name to search for 8653 * @return {ICAL.Time[]} The extracted dates. 8654 */ 8655 _extractDates: function(component, propertyName) { 8656 function handleProp(prop) { 8657 idx = ICAL.helpers.binsearchInsert( 8658 result, 8659 prop, 8660 compareTime 8661 ); 8662 8663 // ordered insert 8664 result.splice(idx, 0, prop); 8665 } 8666 8667 var result = []; 8668 var props = component.getAllProperties(propertyName); 8669 var len = props.length; 8670 var i = 0; 8671 var prop; 8672 8673 var idx; 8674 8675 for (; i < len; i++) { 8676 props[i].getValues().forEach(handleProp); 8677 } 8678 8679 return result; 8680 }, 8681 8682 /** 8683 * Initialize the recurrence expansion. 8684 * 8685 * @private 8686 * @param {ICAL.Component} component The component to initialize from. 8687 */ 8688 _init: function(component) { 8689 this.ruleIterators = []; 8690 8691 this.last = this.dtstart.clone(); 8692 8693 // to provide api consistency non-recurring 8694 // events can also use the iterator though it will 8695 // only return a single time. 8696 if (!isRecurringComponent(component)) { 8697 this.ruleDate = this.last.clone(); 8698 this.complete = true; 8699 return; 8700 } 8701 8702 if (component.hasProperty('rdate')) { 8703 this.ruleDates = this._extractDates(component, 'rdate'); 8704 8705 // special hack for cases where first rdate is prior 8706 // to the start date. We only check for the first rdate. 8707 // This is mostly for google's crazy recurring date logic 8708 // (contacts birthdays). 8709 if ((this.ruleDates[0]) && 8710 (this.ruleDates[0].compare(this.dtstart) < 0)) { 8711 8712 this.ruleDateInc = 0; 8713 this.last = this.ruleDates[0].clone(); 8714 } else { 8715 this.ruleDateInc = ICAL.helpers.binsearchInsert( 8716 this.ruleDates, 8717 this.last, 8718 compareTime 8719 ); 8720 } 8721 8722 this.ruleDate = this.ruleDates[this.ruleDateInc]; 8723 } 8724 8725 if (component.hasProperty('rrule')) { 8726 var rules = component.getAllProperties('rrule'); 8727 var i = 0; 8728 var len = rules.length; 8729 8730 var rule; 8731 var iter; 8732 8733 for (; i < len; i++) { 8734 rule = rules[i].getFirstValue(); 8735 iter = rule.iterator(this.dtstart); 8736 this.ruleIterators.push(iter); 8737 8738 // increment to the next occurrence so future 8739 // calls to next return times beyond the initial iteration. 8740 // XXX: I find this suspicious might be a bug? 8741 iter.next(); 8742 } 8743 } 8744 8745 if (component.hasProperty('exdate')) { 8746 this.exDates = this._extractDates(component, 'exdate'); 8747 // if we have a .last day we increment the index to beyond it. 8748 this.exDateInc = ICAL.helpers.binsearchInsert( 8749 this.exDates, 8750 this.last, 8751 compareTime 8752 ); 8753 8754 this.exDate = this.exDates[this.exDateInc]; 8755 } 8756 }, 8757 8758 /** 8759 * Advance to the next exdate 8760 * @private 8761 */ 8762 _nextExDay: function() { 8763 this.exDate = this.exDates[++this.exDateInc]; 8764 }, 8765 8766 /** 8767 * Advance to the next rule date 8768 * @private 8769 */ 8770 _nextRuleDay: function() { 8771 this.ruleDate = this.ruleDates[++this.ruleDateInc]; 8772 }, 8773 8774 /** 8775 * Find and return the recurrence rule with the most recent event and 8776 * return it. 8777 * 8778 * @private 8779 * @return {?ICAL.RecurIterator} Found iterator. 8780 */ 8781 _nextRecurrenceIter: function() { 8782 var iters = this.ruleIterators; 8783 8784 if (iters.length === 0) { 8785 return null; 8786 } 8787 8788 var len = iters.length; 8789 var iter; 8790 var iterTime; 8791 var iterIdx = 0; 8792 var chosenIter; 8793 8794 // loop through each iterator 8795 for (; iterIdx < len; iterIdx++) { 8796 iter = iters[iterIdx]; 8797 iterTime = iter.last; 8798 8799 // if iteration is complete 8800 // then we must exclude it from 8801 // the search and remove it. 8802 if (iter.completed) { 8803 len--; 8804 if (iterIdx !== 0) { 8805 iterIdx--; 8806 } 8807 iters.splice(iterIdx, 1); 8808 continue; 8809 } 8810 8811 // find the most recent possible choice 8812 if (!chosenIter || chosenIter.last.compare(iterTime) > 0) { 8813 // that iterator is saved 8814 chosenIter = iter; 8815 } 8816 } 8817 8818 // the chosen iterator is returned but not mutated 8819 // this iterator contains the most recent event. 8820 return chosenIter; 8821 } 8822 }; 8823 8824 return RecurExpansion; 8825}()); 8826/* This Source Code Form is subject to the terms of the Mozilla Public 8827 * License, v. 2.0. If a copy of the MPL was not distributed with this 8828 * file, You can obtain one at http://mozilla.org/MPL/2.0/. 8829 * Portions Copyright (C) Philipp Kewisch, 2011-2015 */ 8830 8831 8832/** 8833 * This symbol is further described later on 8834 * @ignore 8835 */ 8836ICAL.Event = (function() { 8837 8838 /** 8839 * @classdesc 8840 * ICAL.js is organized into multiple layers. The bottom layer is a raw jCal 8841 * object, followed by the component/property layer. The highest level is the 8842 * event representation, which this class is part of. See the 8843 * {@tutorial layers} guide for more details. 8844 * 8845 * @class 8846 * @alias ICAL.Event 8847 * @param {ICAL.Component=} component The ICAL.Component to base this event on 8848 * @param {Object} options Options for this event 8849 * @param {Boolean} options.strictExceptions 8850 * When true, will verify exceptions are related by their UUID 8851 * @param {Array<ICAL.Component|ICAL.Event>} options.exceptions 8852 * Exceptions to this event, either as components or events. If not 8853 * specified exceptions will automatically be set in relation of 8854 * component's parent 8855 */ 8856 function Event(component, options) { 8857 if (!(component instanceof ICAL.Component)) { 8858 options = component; 8859 component = null; 8860 } 8861 8862 if (component) { 8863 this.component = component; 8864 } else { 8865 this.component = new ICAL.Component('vevent'); 8866 } 8867 8868 this._rangeExceptionCache = Object.create(null); 8869 this.exceptions = Object.create(null); 8870 this.rangeExceptions = []; 8871 8872 if (options && options.strictExceptions) { 8873 this.strictExceptions = options.strictExceptions; 8874 } 8875 8876 if (options && options.exceptions) { 8877 options.exceptions.forEach(this.relateException, this); 8878 } else if (this.component.parent && !this.isRecurrenceException()) { 8879 this.component.parent.getAllSubcomponents('vevent').forEach(function(event) { 8880 if (event.hasProperty('recurrence-id')) { 8881 this.relateException(event); 8882 } 8883 }, this); 8884 } 8885 } 8886 8887 Event.prototype = { 8888 8889 THISANDFUTURE: 'THISANDFUTURE', 8890 8891 /** 8892 * List of related event exceptions. 8893 * 8894 * @type {ICAL.Event[]} 8895 */ 8896 exceptions: null, 8897 8898 /** 8899 * When true, will verify exceptions are related by their UUID. 8900 * 8901 * @type {Boolean} 8902 */ 8903 strictExceptions: false, 8904 8905 /** 8906 * Relates a given event exception to this object. If the given component 8907 * does not share the UID of this event it cannot be related and will throw 8908 * an exception. 8909 * 8910 * If this component is an exception it cannot have other exceptions 8911 * related to it. 8912 * 8913 * @param {ICAL.Component|ICAL.Event} obj Component or event 8914 */ 8915 relateException: function(obj) { 8916 if (this.isRecurrenceException()) { 8917 throw new Error('cannot relate exception to exceptions'); 8918 } 8919 8920 if (obj instanceof ICAL.Component) { 8921 obj = new ICAL.Event(obj); 8922 } 8923 8924 if (this.strictExceptions && obj.uid !== this.uid) { 8925 throw new Error('attempted to relate unrelated exception'); 8926 } 8927 8928 var id = obj.recurrenceId.toString(); 8929 8930 // we don't sort or manage exceptions directly 8931 // here the recurrence expander handles that. 8932 this.exceptions[id] = obj; 8933 8934 // index RANGE=THISANDFUTURE exceptions so we can 8935 // look them up later in getOccurrenceDetails. 8936 if (obj.modifiesFuture()) { 8937 var item = [ 8938 obj.recurrenceId.toUnixTime(), id 8939 ]; 8940 8941 // we keep them sorted so we can find the nearest 8942 // value later on... 8943 var idx = ICAL.helpers.binsearchInsert( 8944 this.rangeExceptions, 8945 item, 8946 compareRangeException 8947 ); 8948 8949 this.rangeExceptions.splice(idx, 0, item); 8950 } 8951 }, 8952 8953 /** 8954 * Checks if this record is an exception and has the RANGE=THISANDFUTURE 8955 * value. 8956 * 8957 * @return {Boolean} True, when exception is within range 8958 */ 8959 modifiesFuture: function() { 8960 if (!this.component.hasProperty('recurrence-id')) { 8961 return false; 8962 } 8963 8964 var range = this.component.getFirstProperty('recurrence-id').getParameter('range'); 8965 return range === this.THISANDFUTURE; 8966 }, 8967 8968 /** 8969 * Finds the range exception nearest to the given date. 8970 * 8971 * @param {ICAL.Time} time usually an occurrence time of an event 8972 * @return {?ICAL.Event} the related event/exception or null 8973 */ 8974 findRangeException: function(time) { 8975 if (!this.rangeExceptions.length) { 8976 return null; 8977 } 8978 8979 var utc = time.toUnixTime(); 8980 var idx = ICAL.helpers.binsearchInsert( 8981 this.rangeExceptions, 8982 [utc], 8983 compareRangeException 8984 ); 8985 8986 idx -= 1; 8987 8988 // occurs before 8989 if (idx < 0) { 8990 return null; 8991 } 8992 8993 var rangeItem = this.rangeExceptions[idx]; 8994 8995 /* istanbul ignore next: sanity check only */ 8996 if (utc < rangeItem[0]) { 8997 return null; 8998 } 8999 9000 return rangeItem[1]; 9001 }, 9002 9003 /** 9004 * This object is returned by {@link ICAL.Event#getOccurrenceDetails getOccurrenceDetails} 9005 * 9006 * @typedef {Object} occurrenceDetails 9007 * @memberof ICAL.Event 9008 * @property {ICAL.Time} recurrenceId The passed in recurrence id 9009 * @property {ICAL.Event} item The occurrence 9010 * @property {ICAL.Time} startDate The start of the occurrence 9011 * @property {ICAL.Time} endDate The end of the occurrence 9012 */ 9013 9014 /** 9015 * Returns the occurrence details based on its start time. If the 9016 * occurrence has an exception will return the details for that exception. 9017 * 9018 * NOTE: this method is intend to be used in conjunction 9019 * with the {@link ICAL.Event#iterator iterator} method. 9020 * 9021 * @param {ICAL.Time} occurrence time occurrence 9022 * @return {ICAL.Event.occurrenceDetails} Information about the occurrence 9023 */ 9024 getOccurrenceDetails: function(occurrence) { 9025 var id = occurrence.toString(); 9026 var utcId = occurrence.convertToZone(ICAL.Timezone.utcTimezone).toString(); 9027 var item; 9028 var result = { 9029 //XXX: Clone? 9030 recurrenceId: occurrence 9031 }; 9032 9033 if (id in this.exceptions) { 9034 item = result.item = this.exceptions[id]; 9035 result.startDate = item.startDate; 9036 result.endDate = item.endDate; 9037 result.item = item; 9038 } else if (utcId in this.exceptions) { 9039 item = this.exceptions[utcId]; 9040 result.startDate = item.startDate; 9041 result.endDate = item.endDate; 9042 result.item = item; 9043 } else { 9044 // range exceptions (RANGE=THISANDFUTURE) have a 9045 // lower priority then direct exceptions but 9046 // must be accounted for first. Their item is 9047 // always the first exception with the range prop. 9048 var rangeExceptionId = this.findRangeException( 9049 occurrence 9050 ); 9051 var end; 9052 9053 if (rangeExceptionId) { 9054 var exception = this.exceptions[rangeExceptionId]; 9055 9056 // range exception must modify standard time 9057 // by the difference (if any) in start/end times. 9058 result.item = exception; 9059 9060 var startDiff = this._rangeExceptionCache[rangeExceptionId]; 9061 9062 if (!startDiff) { 9063 var original = exception.recurrenceId.clone(); 9064 var newStart = exception.startDate.clone(); 9065 9066 // zones must be same otherwise subtract may be incorrect. 9067 original.zone = newStart.zone; 9068 startDiff = newStart.subtractDate(original); 9069 9070 this._rangeExceptionCache[rangeExceptionId] = startDiff; 9071 } 9072 9073 var start = occurrence.clone(); 9074 start.zone = exception.startDate.zone; 9075 start.addDuration(startDiff); 9076 9077 end = start.clone(); 9078 end.addDuration(exception.duration); 9079 9080 result.startDate = start; 9081 result.endDate = end; 9082 } else { 9083 // no range exception standard expansion 9084 end = occurrence.clone(); 9085 end.addDuration(this.duration); 9086 9087 result.endDate = end; 9088 result.startDate = occurrence; 9089 result.item = this; 9090 } 9091 } 9092 9093 return result; 9094 }, 9095 9096 /** 9097 * Builds a recur expansion instance for a specific point in time (defaults 9098 * to startDate). 9099 * 9100 * @param {ICAL.Time} startTime Starting point for expansion 9101 * @return {ICAL.RecurExpansion} Expansion object 9102 */ 9103 iterator: function(startTime) { 9104 return new ICAL.RecurExpansion({ 9105 component: this.component, 9106 dtstart: startTime || this.startDate 9107 }); 9108 }, 9109 9110 /** 9111 * Checks if the event is recurring 9112 * 9113 * @return {Boolean} True, if event is recurring 9114 */ 9115 isRecurring: function() { 9116 var comp = this.component; 9117 return comp.hasProperty('rrule') || comp.hasProperty('rdate'); 9118 }, 9119 9120 /** 9121 * Checks if the event describes a recurrence exception. See 9122 * {@tutorial terminology} for details. 9123 * 9124 * @return {Boolean} True, if the even describes a recurrence exception 9125 */ 9126 isRecurrenceException: function() { 9127 return this.component.hasProperty('recurrence-id'); 9128 }, 9129 9130 /** 9131 * Returns the types of recurrences this event may have. 9132 * 9133 * Returned as an object with the following possible keys: 9134 * 9135 * - YEARLY 9136 * - MONTHLY 9137 * - WEEKLY 9138 * - DAILY 9139 * - MINUTELY 9140 * - SECONDLY 9141 * 9142 * @return {Object.<ICAL.Recur.frequencyValues, Boolean>} 9143 * Object of recurrence flags 9144 */ 9145 getRecurrenceTypes: function() { 9146 var rules = this.component.getAllProperties('rrule'); 9147 var i = 0; 9148 var len = rules.length; 9149 var result = Object.create(null); 9150 9151 for (; i < len; i++) { 9152 var value = rules[i].getFirstValue(); 9153 result[value.freq] = true; 9154 } 9155 9156 return result; 9157 }, 9158 9159 /** 9160 * The uid of this event 9161 * @type {String} 9162 */ 9163 get uid() { 9164 return this._firstProp('uid'); 9165 }, 9166 9167 set uid(value) { 9168 this._setProp('uid', value); 9169 }, 9170 9171 /** 9172 * The start date 9173 * @type {ICAL.Time} 9174 */ 9175 get startDate() { 9176 return this._firstProp('dtstart'); 9177 }, 9178 9179 set startDate(value) { 9180 this._setTime('dtstart', value); 9181 }, 9182 9183 /** 9184 * The end date. This can be the result directly from the property, or the 9185 * end date calculated from start date and duration. Setting the property 9186 * will remove any duration properties. 9187 * @type {ICAL.Time} 9188 */ 9189 get endDate() { 9190 var endDate = this._firstProp('dtend'); 9191 if (!endDate) { 9192 var duration = this._firstProp('duration'); 9193 endDate = this.startDate.clone(); 9194 if (duration) { 9195 endDate.addDuration(duration); 9196 } else if (endDate.isDate) { 9197 endDate.day += 1; 9198 } 9199 } 9200 return endDate; 9201 }, 9202 9203 set endDate(value) { 9204 if (this.component.hasProperty('duration')) { 9205 this.component.removeProperty('duration'); 9206 } 9207 this._setTime('dtend', value); 9208 }, 9209 9210 /** 9211 * The duration. This can be the result directly from the property, or the 9212 * duration calculated from start date and end date. Setting the property 9213 * will remove any `dtend` properties. 9214 * @type {ICAL.Duration} 9215 */ 9216 get duration() { 9217 var duration = this._firstProp('duration'); 9218 if (!duration) { 9219 return this.endDate.subtractDateTz(this.startDate); 9220 } 9221 return duration; 9222 }, 9223 9224 set duration(value) { 9225 if (this.component.hasProperty('dtend')) { 9226 this.component.removeProperty('dtend'); 9227 } 9228 9229 this._setProp('duration', value); 9230 }, 9231 9232 /** 9233 * The location of the event. 9234 * @type {String} 9235 */ 9236 get location() { 9237 return this._firstProp('location'); 9238 }, 9239 9240 set location(value) { 9241 return this._setProp('location', value); 9242 }, 9243 9244 /** 9245 * The attendees in the event 9246 * @type {ICAL.Property[]} 9247 * @readonly 9248 */ 9249 get attendees() { 9250 //XXX: This is way lame we should have a better 9251 // data structure for this later. 9252 return this.component.getAllProperties('attendee'); 9253 }, 9254 9255 9256 /** 9257 * The event summary 9258 * @type {String} 9259 */ 9260 get summary() { 9261 return this._firstProp('summary'); 9262 }, 9263 9264 set summary(value) { 9265 this._setProp('summary', value); 9266 }, 9267 9268 /** 9269 * The event description. 9270 * @type {String} 9271 */ 9272 get description() { 9273 return this._firstProp('description'); 9274 }, 9275 9276 set description(value) { 9277 this._setProp('description', value); 9278 }, 9279 9280 /** 9281 * The organizer value as an uri. In most cases this is a mailto: uri, but 9282 * it can also be something else, like urn:uuid:... 9283 * @type {String} 9284 */ 9285 get organizer() { 9286 return this._firstProp('organizer'); 9287 }, 9288 9289 set organizer(value) { 9290 this._setProp('organizer', value); 9291 }, 9292 9293 /** 9294 * The sequence value for this event. Used for scheduling 9295 * see {@tutorial terminology}. 9296 * @type {Number} 9297 */ 9298 get sequence() { 9299 return this._firstProp('sequence'); 9300 }, 9301 9302 set sequence(value) { 9303 this._setProp('sequence', value); 9304 }, 9305 9306 /** 9307 * The recurrence id for this event. See {@tutorial terminology} for details. 9308 * @type {ICAL.Time} 9309 */ 9310 get recurrenceId() { 9311 return this._firstProp('recurrence-id'); 9312 }, 9313 9314 set recurrenceId(value) { 9315 this._setTime('recurrence-id', value); 9316 }, 9317 9318 /** 9319 * Set/update a time property's value. 9320 * This will also update the TZID of the property. 9321 * 9322 * TODO: this method handles the case where we are switching 9323 * from a known timezone to an implied timezone (one without TZID). 9324 * This does _not_ handle the case of moving between a known 9325 * (by TimezoneService) timezone to an unknown timezone... 9326 * 9327 * We will not add/remove/update the VTIMEZONE subcomponents 9328 * leading to invalid ICAL data... 9329 * @private 9330 * @param {String} propName The property name 9331 * @param {ICAL.Time} time The time to set 9332 */ 9333 _setTime: function(propName, time) { 9334 var prop = this.component.getFirstProperty(propName); 9335 9336 if (!prop) { 9337 prop = new ICAL.Property(propName); 9338 this.component.addProperty(prop); 9339 } 9340 9341 // utc and local don't get a tzid 9342 if ( 9343 time.zone === ICAL.Timezone.localTimezone || 9344 time.zone === ICAL.Timezone.utcTimezone 9345 ) { 9346 // remove the tzid 9347 prop.removeParameter('tzid'); 9348 } else { 9349 prop.setParameter('tzid', time.zone.tzid); 9350 } 9351 9352 prop.setValue(time); 9353 }, 9354 9355 _setProp: function(name, value) { 9356 this.component.updatePropertyWithValue(name, value); 9357 }, 9358 9359 _firstProp: function(name) { 9360 return this.component.getFirstPropertyValue(name); 9361 }, 9362 9363 /** 9364 * The string representation of this event. 9365 * @return {String} 9366 */ 9367 toString: function() { 9368 return this.component.toString(); 9369 } 9370 9371 }; 9372 9373 function compareRangeException(a, b) { 9374 if (a[0] > b[0]) return 1; 9375 if (b[0] > a[0]) return -1; 9376 return 0; 9377 } 9378 9379 return Event; 9380}()); 9381/* This Source Code Form is subject to the terms of the Mozilla Public 9382 * License, v. 2.0. If a copy of the MPL was not distributed with this 9383 * file, You can obtain one at http://mozilla.org/MPL/2.0/. 9384 * Portions Copyright (C) Philipp Kewisch, 2011-2015 */ 9385 9386 9387/** 9388 * This symbol is further described later on 9389 * @ignore 9390 */ 9391ICAL.ComponentParser = (function() { 9392 /** 9393 * @classdesc 9394 * The ComponentParser is used to process a String or jCal Object, 9395 * firing callbacks for various found components, as well as completion. 9396 * 9397 * @example 9398 * var options = { 9399 * // when false no events will be emitted for type 9400 * parseEvent: true, 9401 * parseTimezone: true 9402 * }; 9403 * 9404 * var parser = new ICAL.ComponentParser(options); 9405 * 9406 * parser.onevent(eventComponent) { 9407 * //... 9408 * } 9409 * 9410 * // ontimezone, etc... 9411 * 9412 * parser.oncomplete = function() { 9413 * 9414 * }; 9415 * 9416 * parser.process(stringOrComponent); 9417 * 9418 * @class 9419 * @alias ICAL.ComponentParser 9420 * @param {Object=} options Component parser options 9421 * @param {Boolean} options.parseEvent Whether events should be parsed 9422 * @param {Boolean} options.parseTimezeone Whether timezones should be parsed 9423 */ 9424 function ComponentParser(options) { 9425 if (typeof(options) === 'undefined') { 9426 options = {}; 9427 } 9428 9429 var key; 9430 for (key in options) { 9431 /* istanbul ignore else */ 9432 if (options.hasOwnProperty(key)) { 9433 this[key] = options[key]; 9434 } 9435 } 9436 } 9437 9438 ComponentParser.prototype = { 9439 9440 /** 9441 * When true, parse events 9442 * 9443 * @type {Boolean} 9444 */ 9445 parseEvent: true, 9446 9447 /** 9448 * When true, parse timezones 9449 * 9450 * @type {Boolean} 9451 */ 9452 parseTimezone: true, 9453 9454 9455 /* SAX like events here for reference */ 9456 9457 /** 9458 * Fired when parsing is complete 9459 * @callback 9460 */ 9461 oncomplete: /* istanbul ignore next */ function() {}, 9462 9463 /** 9464 * Fired if an error occurs during parsing. 9465 * 9466 * @callback 9467 * @param {Error} err details of error 9468 */ 9469 onerror: /* istanbul ignore next */ function(err) {}, 9470 9471 /** 9472 * Fired when a top level component (VTIMEZONE) is found 9473 * 9474 * @callback 9475 * @param {ICAL.Timezone} component Timezone object 9476 */ 9477 ontimezone: /* istanbul ignore next */ function(component) {}, 9478 9479 /** 9480 * Fired when a top level component (VEVENT) is found. 9481 * 9482 * @callback 9483 * @param {ICAL.Event} component Top level component 9484 */ 9485 onevent: /* istanbul ignore next */ function(component) {}, 9486 9487 /** 9488 * Process a string or parse ical object. This function itself will return 9489 * nothing but will start the parsing process. 9490 * 9491 * Events must be registered prior to calling this method. 9492 * 9493 * @param {ICAL.Component|String|Object} ical The component to process, 9494 * either in its final form, as a jCal Object, or string representation 9495 */ 9496 process: function(ical) { 9497 //TODO: this is sync now in the future we will have a incremental parser. 9498 if (typeof(ical) === 'string') { 9499 ical = ICAL.parse(ical); 9500 } 9501 9502 if (!(ical instanceof ICAL.Component)) { 9503 ical = new ICAL.Component(ical); 9504 } 9505 9506 var components = ical.getAllSubcomponents(); 9507 var i = 0; 9508 var len = components.length; 9509 var component; 9510 9511 for (; i < len; i++) { 9512 component = components[i]; 9513 9514 switch (component.name) { 9515 case 'vtimezone': 9516 if (this.parseTimezone) { 9517 var tzid = component.getFirstPropertyValue('tzid'); 9518 if (tzid) { 9519 this.ontimezone(new ICAL.Timezone({ 9520 tzid: tzid, 9521 component: component 9522 })); 9523 } 9524 } 9525 break; 9526 case 'vevent': 9527 if (this.parseEvent) { 9528 this.onevent(new ICAL.Event(component)); 9529 } 9530 break; 9531 default: 9532 continue; 9533 } 9534 } 9535 9536 //XXX: ideally we should do a "nextTick" here 9537 // so in all cases this is actually async. 9538 this.oncomplete(); 9539 } 9540 }; 9541 9542 return ComponentParser; 9543}()); 9544