(function(name, definition)()

in js/libs/ical.js [1:452]


(function(name, definition) {

  /****************
   *  A tolerant, minimal icalendar parser
   *  (http://tools.ietf.org/html/rfc5545)
   *
   *  <peterbraden@peterbraden.co.uk>
   * **************/
  
    if (typeof module !== 'undefined') {
      module.exports = definition();
    } else if (typeof define === 'function' && typeof define.amd === 'object'){
      define(definition);
    } else {
      this[name] = definition();
    }
  
  }('ical', function(){
  
     // Unescape Text re RFC 4.3.11
    var text = function(t){
      t = t || "";
      return (t
        .replace(/\\\,/g, ',')
        .replace(/\\\;/g, ';')
        .replace(/\\[nN]/g, '\n')
        .replace(/\\\\/g, '\\')
      )
    }
  
    var parseParams = function(p){
      var out = {}
      for (var i = 0; i<p.length; i++){
        if (p[i].indexOf('=') > -1){
          var segs = p[i].split('=');
  
          out[segs[0]] = parseValue(segs.slice(1).join('='));
  
        }
      }
      return out || sp
    }
  
    var parseValue = function(val){
      if ('TRUE' === val)
        return true;
  
      if ('FALSE' === val)
        return false;
  
      var number = Number(val);
      if (!isNaN(number))
        return number;
  
      return val;
    }
  
    var storeValParam = function (name) {
        return function (val, curr) {
            var current = curr[name];
            if (Array.isArray(current)) {
                current.push(val);
                return curr;
            }
  
            if (current != null) {
                curr[name] = [current, val];
                return curr;
            }
  
            curr[name] = val;
            return curr
        }
    }
  
    var storeParam = function (name) {
        return function (val, params, curr) {
            var data;
            if (params && params.length && !(params.length == 1 && params[0] === 'CHARSET=utf-8')) {
                data = { params: parseParams(params), val: text(val) }
            }
            else
                data = text(val)
  
            return storeValParam(name)(data, curr);
        }
    }
  
    var addTZ = function (dt, params) {
      var p = parseParams(params);
  
      if (params && p){
        dt.tz = p.TZID
      }
  
      return dt
    }
  
    var dateParam = function(name){
        return function (val, params, curr) {
  
        var newDate = text(val);
  
  
        if (params && params[0] === "VALUE=DATE") {
          // Just Date
  
          var comps = /^(\d{4})(\d{2})(\d{2})$/.exec(val);
          if (comps !== null) {
            // No TZ info - assume same timezone as this computer
            newDate = new Date(
              comps[1],
              parseInt(comps[2], 10)-1,
              comps[3]
            );
  
            newDate = addTZ(newDate, params);
            newDate.dateOnly = true;
  
            // Store as string - worst case scenario
            return storeValParam(name)(newDate, curr)
          }
        }
  
  
        //typical RFC date-time format
        var comps = /^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})(Z)?$/.exec(val);
        if (comps !== null) {
          if (comps[7] == 'Z'){ // GMT
            newDate = new Date(Date.UTC(
              parseInt(comps[1], 10),
              parseInt(comps[2], 10)-1,
              parseInt(comps[3], 10),
              parseInt(comps[4], 10),
              parseInt(comps[5], 10),
              parseInt(comps[6], 10 )
            ));
            // TODO add tz
          } else {
            newDate = new Date(
              parseInt(comps[1], 10),
              parseInt(comps[2], 10)-1,
              parseInt(comps[3], 10),
              parseInt(comps[4], 10),
              parseInt(comps[5], 10),
              parseInt(comps[6], 10)
            );
          }
  
          newDate = addTZ(newDate, params);
      }
  
  
            // Store as string - worst case scenario
        return storeValParam(name)(newDate, curr)
        }
    }
  
  
    var geoParam = function(name){
      return function(val, params, curr){
        storeParam(val, params, curr)
        var parts = val.split(';');
        curr[name] = {lat:Number(parts[0]), lon:Number(parts[1])};
        return curr
      }
    }
  
    var categoriesParam = function (name) {
      var separatorPattern = /\s*,\s*/g;
      return function (val, params, curr) {
        storeParam(val, params, curr)
        if (curr[name] === undefined)
          curr[name] = val ? val.split(separatorPattern) : []
        else
          if (val)
            curr[name] = curr[name].concat(val.split(separatorPattern))
        return curr
      }
    }
  
    // EXDATE is an entry that represents exceptions to a recurrence rule (ex: "repeat every day except on 7/4").
    // The EXDATE entry itself can also contain a comma-separated list, so we make sure to parse each date out separately.
    // There can also be more than one EXDATE entries in a calendar record.
    // Since there can be multiple dates, we create an array of them.  The index into the array is the ISO string of the date itself, for ease of use.
    // i.e. You can check if ((curr.exdate != undefined) && (curr.exdate[date iso string] != undefined)) to see if a date is an exception.
    // NOTE: This specifically uses date only, and not time.  This is to avoid a few problems:
    //    1. The ISO string with time wouldn't work for "floating dates" (dates without timezones).
    //       ex: "20171225T060000" - this is supposed to mean 6 AM in whatever timezone you're currently in
    //    2. Daylight savings time potentially affects the time you would need to look up
    //    3. Some EXDATE entries in the wild seem to have times different from the recurrence rule, but are still excluded by calendar programs.  Not sure how or why.
    //       These would fail any sort of sane time lookup, because the time literally doesn't match the event.  So we'll ignore time and just use date.
    //       ex: DTSTART:20170814T140000Z
    //             RRULE:FREQ=WEEKLY;WKST=SU;INTERVAL=2;BYDAY=MO,TU
    //             EXDATE:20171219T060000
    //       Even though "T060000" doesn't match or overlap "T1400000Z", it's still supposed to be excluded?  Odd. :(
    // TODO: See if this causes any problems with events that recur multiple times a day.
    var exdateParam = function (name) {
      return function (val, params, curr) {
        var separatorPattern = /\s*,\s*/g;
        curr[name] = curr[name] || [];
        var dates = val ? val.split(separatorPattern) : [];
        dates.forEach(function (entry) {
            var exdate = new Array();
            dateParam(name)(entry, params, exdate);
  
            if (exdate[name])
            {
              if (typeof exdate[name].toISOString === 'function') {
                curr[name][exdate[name].toISOString().substring(0, 10)] = exdate[name];
              } else {
                console.error("No toISOString function in exdate[name]", exdate[name]);
              }
            }
          }
        )
        return curr;
      }
    }
  
    // RECURRENCE-ID is the ID of a specific recurrence within a recurrence rule.
    // TODO:  It's also possible for it to have a range, like "THISANDPRIOR", "THISANDFUTURE".  This isn't currently handled.
    var recurrenceParam = function (name) {
        return dateParam(name);
    }
  
    var addFBType = function (fb, params) {
      var p = parseParams(params);
  
      if (params && p){
        fb.type = p.FBTYPE || "BUSY"
      }
  
      return fb;
    }
  
    var freebusyParam = function (name) {
      return function(val, params, curr){
        var fb = addFBType({}, params);
        curr[name] = curr[name] || []
        curr[name].push(fb);
  
        storeParam(val, params, fb);
  
        var parts = val.split('/');
  
        ['start', 'end'].forEach(function (name, index) {
          dateParam(name)(parts[index], params, fb);
        });
  
        return curr;
      }
    }
  
    return {
  
  
      objectHandlers : {
        'BEGIN' : function(component, params, curr, stack){
            stack.push(curr)
  
            return {type:component, params:params}
          }
  
        , 'END' : function(component, params, curr, stack){
          // prevents the need to search the root of the tree for the VCALENDAR object
          if (component === "VCALENDAR") {
              //scan all high level object in curr and drop all strings
              var key,
                  obj;
  
              for (key in curr) {
                  if(curr.hasOwnProperty(key)) {
                     obj = curr[key];
                     if (typeof obj === 'string') {
                         delete curr[key];
                     }
                  }
              }
  
              return curr
          }
  
          var par = stack.pop()
  
          if (curr.uid)
          {
            // If this is the first time we run into this UID, just save it.
            if (par[curr.uid] === undefined)
              {
                par[curr.uid] = curr;
              }
              else
              {
                  // If we have multiple ical entries with the same UID, it's either going to be a
                  // modification to a recurrence (RECURRENCE-ID), and/or a significant modification
                  // to the entry (SEQUENCE).
  
                  // TODO: Look into proper sequence logic.
  
                  if (curr.recurrenceid === undefined)
                  {
                      // If we have the same UID as an existing record, and it *isn't* a specific recurrence ID,
                      // not quite sure what the correct behaviour should be.  For now, just take the new information
                      // and merge it with the old record by overwriting only the fields that appear in the new record.
                      var key;
                      for (key in curr) {
                        par[curr.uid][key] = curr[key];
                      }
  
                  }
              }
  
            // If we have recurrence-id entries, list them as an array of recurrences keyed off of recurrence-id.
            // To use - as you're running through the dates of an rrule, you can try looking it up in the recurrences
            // array.  If it exists, then use the data from the calendar object in the recurrence instead of the parent
            // for that day.
  
            // NOTE:  Sometimes the RECURRENCE-ID record will show up *before* the record with the RRULE entry.  In that
            // case, what happens is that the RECURRENCE-ID record ends up becoming both the parent record and an entry
            // in the recurrences array, and then when we process the RRULE entry later it overwrites the appropriate
        // fields in the parent record.
  
            if (curr.recurrenceid != null)
            {
  
              // TODO:  Is there ever a case where we have to worry about overwriting an existing entry here?
  
              // Create a copy of the current object to save in our recurrences array.  (We *could* just do par = curr,
              // except for the case that we get the RECURRENCE-ID record before the RRULE record.  In that case, we
              // would end up with a shared reference that would cause us to overwrite *both* records at the point
          // that we try and fix up the parent record.)
              var recurrenceObj = new Object();
              var key;
              for (key in curr) {
                recurrenceObj[key] = curr[key];
              }
  
              if (recurrenceObj.recurrences != undefined) {
                delete recurrenceObj.recurrences;
              }
  
  
          // If we don't have an array to store recurrences in yet, create it.
              if (par[curr.uid].recurrences === undefined) {
                par[curr.uid].recurrences = new Array();
                }
  
              // Save off our cloned recurrence object into the array, keyed by date but not time.
              // We key by date only to avoid timezone and "floating time" problems (where the time isn't associated with a timezone).
          // TODO: See if this causes a problem with events that have multiple recurrences per day.
                  if (typeof curr.recurrenceid.toISOString === 'function') {
                    par[curr.uid].recurrences[curr.recurrenceid.toISOString().substring(0,10)] = recurrenceObj;
                  } else {
                    console.error("No toISOString function in curr.recurrenceid", curr.recurrenceid);
                  }
              }
  
            // One more specific fix - in the case that an RRULE entry shows up after a RECURRENCE-ID entry,
            // let's make sure to clear the recurrenceid off the parent field.
            if ((par[curr.uid].rrule != undefined) && (par[curr.uid].recurrenceid != undefined))
              {
              delete par[curr.uid].recurrenceid;
              }
  
          }
          else
            par[Math.random()*100000] = curr  // Randomly assign ID : TODO - use true GUID
  
          return par
        }
  
        , 'SUMMARY' : storeParam('summary')
        , 'DESCRIPTION' : storeParam('description')
        , 'URL' : storeParam('url')
        , 'UID' : storeParam('uid')
        , 'LOCATION' : storeParam('location')
        , 'DTSTART' : dateParam('start')
        , 'DTEND' : dateParam('end')
        , 'EXDATE' : exdateParam('exdate')
        ,' CLASS' : storeParam('class')
        , 'TRANSP' : storeParam('transparency')
        , 'GEO' : geoParam('geo')
        , 'PERCENT-COMPLETE': storeParam('completion')
        , 'COMPLETED': dateParam('completed')
        , 'CATEGORIES': categoriesParam('categories')
        , 'FREEBUSY': freebusyParam('freebusy')
        , 'DTSTAMP': dateParam('dtstamp')
        , 'CREATED': dateParam('created')
        , 'LAST-MODIFIED': dateParam('lastmodified')
        , 'RECURRENCE-ID': recurrenceParam('recurrenceid')
  
      },
  
  
      handleObject : function(name, val, params, ctx, stack, line){
        var self = this
  
        if(self.objectHandlers[name])
          return self.objectHandlers[name](val, params, ctx, stack, line)
  
        //handling custom properties
        if(name.match(/X\-[\w\-]+/) && stack.length > 0) {
            //trimming the leading and perform storeParam
            name = name.substring(2);
            return (storeParam(name))(val, params, ctx, stack, line);
        }
  
        return storeParam(name.toLowerCase())(val, params, ctx);
      },
  
  
      parseICS : function(str){
        var self = this
        var lines = str.split(/\r?\n/)
        var ctx = {}
        var stack = []
  
        for (var i = 0, ii = lines.length, l = lines[0]; i<ii; i++, l=lines[i]){
          //Unfold : RFC#3.1
          while (lines[i+1] && /[ \t]/.test(lines[i+1][0])) {
            l += lines[i+1].slice(1)
            i += 1
          }
  
          var kv = l.split(":")
  
          if (kv.length < 2){
            // Invalid line - must have k&v
            continue;
          }
  
          // Although the spec says that vals with colons should be quote wrapped
          // in practise nobody does, so we assume further colons are part of the
          // val
          var value = kv.slice(1).join(":")
            , kp = kv[0].split(";")
            , name = kp[0]
            , params = kp.slice(1)
  
          ctx = self.handleObject(name, value, params, ctx, stack, l) || {}
        }
  
         // type and params are added to the list of items, get rid of them.
         delete ctx.type
         delete ctx.params
  
         return ctx
      }
  
    }
  }))