design.js

/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 * Portions Copyright (C) Philipp Kewisch */

import { isStrictlyNaN, extend } from "./helpers.js";
import UtcOffset from "./utc_offset.js";
import VCardTime from "./vcard_time.js";
import Recur from "./recur.js";
import Period from "./period.js";
import Duration from "./duration.js";
import Time from "./time.js";
import Binary from "./binary.js";

/**
 * This lets typescript resolve our custom types in the
 * generated d.ts files (jsdoc typedefs are converted to typescript types).
 * Ignore prevents the typedefs from being documented more than once.
 * @ignore
 * @typedef {import("./types.js").designSet} designSet
 * Imports the 'designSet' type from the "types.js" module
 */

/** @module ICAL.design */

const FROM_ICAL_NEWLINE = /\\\\|\\;|\\,|\\[Nn]/g;
const TO_ICAL_NEWLINE = /\\|;|,|\n/g;
const FROM_VCARD_NEWLINE = /\\\\|\\,|\\[Nn]/g;
const TO_VCARD_NEWLINE = /\\|,|\n/g;

function createTextType(fromNewline, toNewline) {
  let result = {
    matches: /.*/,

    fromICAL: function(aValue, structuredEscape) {
      return replaceNewline(aValue, fromNewline, structuredEscape);
    },

    toICAL: function(aValue, structuredEscape) {
      let regEx = toNewline;
      if (structuredEscape)
         regEx = new RegExp(regEx.source + '|' + structuredEscape, regEx.flags);
      return aValue.replace(regEx, function(str) {
        switch (str) {
        case "\\":
          return "\\\\";
        case ";":
          return "\\;";
        case ",":
          return "\\,";
        case "\n":
          return "\\n";
        /* c8 ignore next 2 */
        default:
          return str;
        }
      });
    }
  };
  return result;
}

// default types used multiple times
const DEFAULT_TYPE_TEXT = { defaultType: "text" };
const DEFAULT_TYPE_TEXT_MULTI = { defaultType: "text", multiValue: "," };
const DEFAULT_TYPE_TEXT_STRUCTURED = { defaultType: "text", structuredValue: ";" };
const DEFAULT_TYPE_INTEGER = { defaultType: "integer" };
const DEFAULT_TYPE_DATETIME_DATE = { defaultType: "date-time", allowedTypes: ["date-time", "date"] };
const DEFAULT_TYPE_DATETIME = { defaultType: "date-time" };
const DEFAULT_TYPE_URI = { defaultType: "uri" };
const DEFAULT_TYPE_UTCOFFSET = { defaultType: "utc-offset" };
const DEFAULT_TYPE_RECUR = { defaultType: "recur" };
const DEFAULT_TYPE_DATE_ANDOR_TIME = { defaultType: "date-and-or-time", allowedTypes: ["date-time", "date", "text"] };

function replaceNewlineReplace(string) {
  switch (string) {
    case "\\\\":
      return "\\";
    case "\\;":
      return ";";
    case "\\,":
      return ",";
    case "\\n":
    case "\\N":
      return "\n";
    /* c8 ignore next 2 */
    default:
      return string;
  }
}

function replaceNewline(value, newline, structuredEscape) {
  // avoid regex when possible.
  if (value.indexOf('\\') === -1) {
    return value;
  }
  if (structuredEscape)
     newline = new RegExp(newline.source + '|\\\\' + structuredEscape, newline.flags);
  return value.replace(newline, replaceNewlineReplace);
}

let commonProperties = {
  "categories": DEFAULT_TYPE_TEXT_MULTI,
  "url": DEFAULT_TYPE_URI,
  "version": DEFAULT_TYPE_TEXT,
  "uid": DEFAULT_TYPE_TEXT
};

let commonValues = {
  "boolean": {
    values: ["TRUE", "FALSE"],

    fromICAL: function(aValue) {
      switch (aValue) {
        case 'TRUE':
          return true;
        case 'FALSE':
          return false;
        default:
          //TODO: parser warning
          return false;
      }
    },

    toICAL: function(aValue) {
      if (aValue) {
        return 'TRUE';
      }
      return 'FALSE';
    }

  },
  float: {
    matches: /^[+-]?\d+\.\d+$/,

    fromICAL: function(aValue) {
      let parsed = parseFloat(aValue);
      if (isStrictlyNaN(parsed)) {
        // TODO: parser warning
        return 0.0;
      }
      return parsed;
    },

    toICAL: function(aValue) {
      return String(aValue);
    }
  },
  integer: {
    fromICAL: function(aValue) {
      let parsed = parseInt(aValue);
      if (isStrictlyNaN(parsed)) {
        return 0;
      }
      return parsed;
    },

    toICAL: function(aValue) {
      return String(aValue);
    }
  },
  "utc-offset": {
    toICAL: function(aValue) {
      if (aValue.length < 7) {
        // no seconds
        // -0500
        return aValue.slice(0, 3) +
               aValue.slice(4, 6);
      } else {
        // seconds
        // -050000
        return aValue.slice(0, 3) +
               aValue.slice(4, 6) +
               aValue.slice(7, 9);
      }
    },

    fromICAL: function(aValue) {
      if (aValue.length < 6) {
        // no seconds
        // -05:00
        return aValue.slice(0, 3) + ':' +
               aValue.slice(3, 5);
      } else {
        // seconds
        // -05:00:00
        return aValue.slice(0, 3) + ':' +
               aValue.slice(3, 5) + ':' +
               aValue.slice(5, 7);
      }
    },

    decorate: function(aValue) {
      return UtcOffset.fromString(aValue);
    },

    undecorate: function(aValue) {
      return aValue.toString();
    }
  }
};

let icalParams = {
  // Although the syntax is DQUOTE uri DQUOTE, I don't think we should
  // enforce anything aside from it being a valid content line.
  //
  // At least some params require - if multi values are used - DQUOTEs
  // for each of its values - e.g. delegated-from="uri1","uri2"
  // To indicate this, I introduced the new k/v pair
  // multiValueSeparateDQuote: true
  //
  // "ALTREP": { ... },

  // CN just wants a param-value
  // "CN": { ... }

  "cutype": {
    values: ["INDIVIDUAL", "GROUP", "RESOURCE", "ROOM", "UNKNOWN"],
    allowXName: true,
    allowIanaToken: true
  },

  "delegated-from": {
    valueType: "cal-address",
    multiValue: ",",
    multiValueSeparateDQuote: true
  },
  "delegated-to": {
    valueType: "cal-address",
    multiValue: ",",
    multiValueSeparateDQuote: true
  },
  // "DIR": { ... }, // See ALTREP
  "encoding": {
    values: ["8BIT", "BASE64"]
  },
  // "FMTTYPE": { ... }, // See ALTREP
  "fbtype": {
    values: ["FREE", "BUSY", "BUSY-UNAVAILABLE", "BUSY-TENTATIVE"],
    allowXName: true,
    allowIanaToken: true
  },
  // "LANGUAGE": { ... }, // See ALTREP
  "member": {
    valueType: "cal-address",
    multiValue: ",",
    multiValueSeparateDQuote: true
  },
  "partstat": {
    // TODO These values are actually different per-component
    values: ["NEEDS-ACTION", "ACCEPTED", "DECLINED", "TENTATIVE",
             "DELEGATED", "COMPLETED", "IN-PROCESS"],
    allowXName: true,
    allowIanaToken: true
  },
  "range": {
    values: ["THISANDFUTURE"]
  },
  "related": {
    values: ["START", "END"]
  },
  "reltype": {
    values: ["PARENT", "CHILD", "SIBLING"],
    allowXName: true,
    allowIanaToken: true
  },
  "role": {
    values: ["REQ-PARTICIPANT", "CHAIR",
             "OPT-PARTICIPANT", "NON-PARTICIPANT"],
    allowXName: true,
    allowIanaToken: true
  },
  "rsvp": {
    values: ["TRUE", "FALSE"]
  },
  "sent-by": {
    valueType: "cal-address"
  },
  "tzid": {
    matches: /^\//
  },
  "value": {
    // since the value here is a 'type' lowercase is used.
    values: ["binary", "boolean", "cal-address", "date", "date-time",
             "duration", "float", "integer", "period", "recur", "text",
             "time", "uri", "utc-offset"],
    allowXName: true,
    allowIanaToken: true
  }
};

// When adding a value here, be sure to add it to the parameter types!
const icalValues = extend(commonValues, {
  text: createTextType(FROM_ICAL_NEWLINE, TO_ICAL_NEWLINE),

  uri: {
    // TODO
    /* ... */
  },

  "binary": {
    decorate: function(aString) {
      return Binary.fromString(aString);
    },

    undecorate: function(aBinary) {
      return aBinary.toString();
    }
  },
  "cal-address": {
    // needs to be an uri
  },
  "date": {
    decorate: function(aValue, aProp) {
      if (design.strict) {
        return Time.fromDateString(aValue, aProp);
      } else {
        return Time.fromString(aValue, aProp);
      }
    },

    /**
     * undecorates a time object.
     */
    undecorate: function(aValue) {
      return aValue.toString();
    },

    fromICAL: function(aValue) {
      // from: 20120901
      // to: 2012-09-01
      if (!design.strict && aValue.length >= 15) {
        // This is probably a date-time, e.g. 20120901T130000Z
        return icalValues["date-time"].fromICAL(aValue);
      } else {
        return aValue.slice(0, 4) + '-' +
               aValue.slice(4, 6) + '-' +
               aValue.slice(6, 8);
      }
    },

    toICAL: function(aValue) {
      // from: 2012-09-01
      // to: 20120901
      let len = aValue.length;

      if (len == 10) {
        return aValue.slice(0, 4) +
               aValue.slice(5, 7) +
               aValue.slice(8, 10);
      } else if (len >= 19) {
        return icalValues["date-time"].toICAL(aValue);
      } else {
        //TODO: serialize warning?
        return aValue;
      }

    }
  },
  "date-time": {
    fromICAL: function(aValue) {
      // from: 20120901T130000
      // to: 2012-09-01T13:00:00
      if (!design.strict && aValue.length == 8) {
        // This is probably a date, e.g. 20120901
        return icalValues.date.fromICAL(aValue);
      } else {
        let result = aValue.slice(0, 4) + '-' +
                     aValue.slice(4, 6) + '-' +
                     aValue.slice(6, 8) + 'T' +
                     aValue.slice(9, 11) + ':' +
                     aValue.slice(11, 13) + ':' +
                     aValue.slice(13, 15);

        if (aValue[15] && aValue[15] === 'Z') {
          result += 'Z';
        }

        return result;
      }
    },

    toICAL: function(aValue) {
      // from: 2012-09-01T13:00:00
      // to: 20120901T130000
      let len = aValue.length;

      if (len == 10 && !design.strict) {
        return icalValues.date.toICAL(aValue);
      } else if (len >= 19) {
        let result = aValue.slice(0, 4) +
                     aValue.slice(5, 7) +
                     // grab the (DDTHH) segment
                     aValue.slice(8, 13) +
                     // MM
                     aValue.slice(14, 16) +
                     // SS
                     aValue.slice(17, 19);

        if (aValue[19] && aValue[19] === 'Z') {
          result += 'Z';
        }
        return result;
      } else {
        // TODO: error
        return aValue;
      }
    },

    decorate: function(aValue, aProp) {
      if (design.strict) {
        return Time.fromDateTimeString(aValue, aProp);
      } else {
        return Time.fromString(aValue, aProp);
      }
    },

    undecorate: function(aValue) {
      return aValue.toString();
    }
  },
  duration: {
    decorate: function(aValue) {
      return Duration.fromString(aValue);
    },
    undecorate: function(aValue) {
      return aValue.toString();
    }
  },
  period: {
    fromICAL: function(string) {
      let parts = string.split('/');
      parts[0] = icalValues['date-time'].fromICAL(parts[0]);

      if (!Duration.isValueString(parts[1])) {
        parts[1] = icalValues['date-time'].fromICAL(parts[1]);
      }

      return parts;
    },

    toICAL: function(parts) {
      parts = parts.slice();
      if (!design.strict && parts[0].length == 10) {
        parts[0] = icalValues.date.toICAL(parts[0]);
      } else {
        parts[0] = icalValues['date-time'].toICAL(parts[0]);
      }

      if (!Duration.isValueString(parts[1])) {
        if (!design.strict && parts[1].length == 10) {
          parts[1] = icalValues.date.toICAL(parts[1]);
        } else {
          parts[1] = icalValues['date-time'].toICAL(parts[1]);
        }
      }

      return parts.join("/");
    },

    decorate: function(aValue, aProp) {
      return Period.fromJSON(aValue, aProp, !design.strict);
    },

    undecorate: function(aValue) {
      return aValue.toJSON();
    }
  },
  recur: {
    fromICAL: function(string) {
      return Recur._stringToData(string, true);
    },

    toICAL: function(data) {
      let str = "";
      for (let [k, val] of Object.entries(data)) {
        if (k == "until") {
          if (val.length > 10) {
            val = icalValues['date-time'].toICAL(val);
          } else {
            val = icalValues.date.toICAL(val);
          }
        } else if (k == "wkst") {
          if (typeof val === 'number') {
            val = Recur.numericDayToIcalDay(val);
          }
        } else if (Array.isArray(val)) {
          val = val.join(",");
        }
        str += k.toUpperCase() + "=" + val + ";";
      }
      return str.slice(0, Math.max(0, str.length - 1));
    },

    decorate: function decorate(aValue) {
      return Recur.fromData(aValue);
    },

    undecorate: function(aRecur) {
      return aRecur.toJSON();
    }
  },

  time: {
    fromICAL: function(aValue) {
      // from: MMHHSS(Z)?
      // to: HH:MM:SS(Z)?
      if (aValue.length < 6) {
        // TODO: parser exception?
        return aValue;
      }

      // HH::MM::SSZ?
      let result = aValue.slice(0, 2) + ':' +
                   aValue.slice(2, 4) + ':' +
                   aValue.slice(4, 6);

      if (aValue[6] === 'Z') {
        result += 'Z';
      }

      return result;
    },

    toICAL: function(aValue) {
      // from: HH:MM:SS(Z)?
      // to: MMHHSS(Z)?
      if (aValue.length < 8) {
        //TODO: error
        return aValue;
      }

      let result = aValue.slice(0, 2) +
                   aValue.slice(3, 5) +
                   aValue.slice(6, 8);

      if (aValue[8] === 'Z') {
        result += 'Z';
      }

      return result;
    }
  }
});

let icalProperties = extend(commonProperties, {

  "action": DEFAULT_TYPE_TEXT,
  "attach": { defaultType: "uri" },
  "attendee": { defaultType: "cal-address" },
  "calscale": DEFAULT_TYPE_TEXT,
  "class": DEFAULT_TYPE_TEXT,
  "comment": DEFAULT_TYPE_TEXT,
  "completed": DEFAULT_TYPE_DATETIME,
  "contact": DEFAULT_TYPE_TEXT,
  "created": DEFAULT_TYPE_DATETIME,
  "description": DEFAULT_TYPE_TEXT,
  "dtend": DEFAULT_TYPE_DATETIME_DATE,
  "dtstamp": DEFAULT_TYPE_DATETIME,
  "dtstart": DEFAULT_TYPE_DATETIME_DATE,
  "due": DEFAULT_TYPE_DATETIME_DATE,
  "duration": { defaultType: "duration" },
  "exdate": {
    defaultType: "date-time",
    allowedTypes: ["date-time", "date"],
    multiValue: ','
  },
  "exrule": DEFAULT_TYPE_RECUR,
  "freebusy": { defaultType: "period", multiValue: "," },
  "geo": { defaultType: "float", structuredValue: ";" },
  "last-modified": DEFAULT_TYPE_DATETIME,
  "location": DEFAULT_TYPE_TEXT,
  "method": DEFAULT_TYPE_TEXT,
  "organizer": { defaultType: "cal-address" },
  "percent-complete": DEFAULT_TYPE_INTEGER,
  "priority": DEFAULT_TYPE_INTEGER,
  "prodid": DEFAULT_TYPE_TEXT,
  "related-to": DEFAULT_TYPE_TEXT,
  "repeat": DEFAULT_TYPE_INTEGER,
  "rdate": {
    defaultType: "date-time",
    allowedTypes: ["date-time", "date", "period"],
    multiValue: ',',
    detectType: function(string) {
      if (string.indexOf('/') !== -1) {
        return 'period';
      }
      return (string.indexOf('T') === -1) ? 'date' : 'date-time';
    }
  },
  "recurrence-id": DEFAULT_TYPE_DATETIME_DATE,
  "resources": DEFAULT_TYPE_TEXT_MULTI,
  "request-status": DEFAULT_TYPE_TEXT_STRUCTURED,
  "rrule": DEFAULT_TYPE_RECUR,
  "sequence": DEFAULT_TYPE_INTEGER,
  "status": DEFAULT_TYPE_TEXT,
  "summary": DEFAULT_TYPE_TEXT,
  "transp": DEFAULT_TYPE_TEXT,
  "trigger": { defaultType: "duration", allowedTypes: ["duration", "date-time"] },
  "tzoffsetfrom": DEFAULT_TYPE_UTCOFFSET,
  "tzoffsetto": DEFAULT_TYPE_UTCOFFSET,
  "tzurl": DEFAULT_TYPE_URI,
  "tzid": DEFAULT_TYPE_TEXT,
  "tzname": DEFAULT_TYPE_TEXT
});

// When adding a value here, be sure to add it to the parameter types!
const vcardValues = extend(commonValues, {
  text: createTextType(FROM_VCARD_NEWLINE, TO_VCARD_NEWLINE),
  uri: createTextType(FROM_VCARD_NEWLINE, TO_VCARD_NEWLINE),

  date: {
    decorate: function(aValue) {
      return VCardTime.fromDateAndOrTimeString(aValue, "date");
    },
    undecorate: function(aValue) {
      return aValue.toString();
    },
    fromICAL: function(aValue) {
      if (aValue.length == 8) {
        return icalValues.date.fromICAL(aValue);
      } else if (aValue[0] == '-' && aValue.length == 6) {
        return aValue.slice(0, 4) + '-' + aValue.slice(4);
      } else {
        return aValue;
      }
    },
    toICAL: function(aValue) {
      if (aValue.length == 10) {
        return icalValues.date.toICAL(aValue);
      } else if (aValue[0] == '-' && aValue.length == 7) {
        return aValue.slice(0, 4) + aValue.slice(5);
      } else {
        return aValue;
      }
    }
  },

  time: {
    decorate: function(aValue) {
      return VCardTime.fromDateAndOrTimeString("T" + aValue, "time");
    },
    undecorate: function(aValue) {
      return aValue.toString();
    },
    fromICAL: function(aValue) {
      let splitzone = vcardValues.time._splitZone(aValue, true);
      let zone = splitzone[0], value = splitzone[1];

      //console.log("SPLIT: ",splitzone);

      if (value.length == 6) {
        value = value.slice(0, 2) + ':' +
                value.slice(2, 4) + ':' +
                value.slice(4, 6);
      } else if (value.length == 4 && value[0] != '-') {
        value = value.slice(0, 2) + ':' + value.slice(2, 4);
      } else if (value.length == 5) {
        value = value.slice(0, 3) + ':' + value.slice(3, 5);
      }

      if (zone.length == 5 && (zone[0] == '-' || zone[0] == '+')) {
        zone = zone.slice(0, 3) + ':' + zone.slice(3);
      }

      return value + zone;
    },

    toICAL: function(aValue) {
      let splitzone = vcardValues.time._splitZone(aValue);
      let zone = splitzone[0], value = splitzone[1];

      if (value.length == 8) {
        value = value.slice(0, 2) +
                value.slice(3, 5) +
                value.slice(6, 8);
      } else if (value.length == 5 && value[0] != '-') {
        value = value.slice(0, 2) + value.slice(3, 5);
      } else if (value.length == 6) {
        value = value.slice(0, 3) + value.slice(4, 6);
      }

      if (zone.length == 6 && (zone[0] == '-' || zone[0] == '+')) {
        zone = zone.slice(0, 3) + zone.slice(4);
      }

      return value + zone;
    },

    _splitZone: function(aValue, isFromIcal) {
      let lastChar = aValue.length - 1;
      let signChar = aValue.length - (isFromIcal ? 5 : 6);
      let sign = aValue[signChar];
      let zone, value;

      if (aValue[lastChar] == 'Z') {
        zone = aValue[lastChar];
        value = aValue.slice(0, Math.max(0, lastChar));
      } else if (aValue.length > 6 && (sign == '-' || sign == '+')) {
        zone = aValue.slice(signChar);
        value = aValue.slice(0, Math.max(0, signChar));
      } else {
        zone = "";
        value = aValue;
      }

      return [zone, value];
    }
  },

  "date-time": {
    decorate: function(aValue) {
      return VCardTime.fromDateAndOrTimeString(aValue, "date-time");
    },

    undecorate: function(aValue) {
      return aValue.toString();
    },

    fromICAL: function(aValue) {
      return vcardValues['date-and-or-time'].fromICAL(aValue);
    },

    toICAL: function(aValue) {
      return vcardValues['date-and-or-time'].toICAL(aValue);
    }
  },

  "date-and-or-time": {
    decorate: function(aValue) {
      return VCardTime.fromDateAndOrTimeString(aValue, "date-and-or-time");
    },

    undecorate: function(aValue) {
      return aValue.toString();
    },

    fromICAL: function(aValue) {
      let parts = aValue.split('T');
      return (parts[0] ? vcardValues.date.fromICAL(parts[0]) : '') +
             (parts[1] ? 'T' + vcardValues.time.fromICAL(parts[1]) : '');
    },

    toICAL: function(aValue) {
      let parts = aValue.split('T');
      return vcardValues.date.toICAL(parts[0]) +
             (parts[1] ? 'T' + vcardValues.time.toICAL(parts[1]) : '');

    }
  },
  timestamp: icalValues['date-time'],
  "language-tag": {
    matches: /^[a-zA-Z0-9-]+$/ // Could go with a more strict regex here
  },
  "phone-number": {
    fromICAL: function(aValue) {
      return Array.from(aValue).filter(function(c) {
          return c === '\\' ? undefined : c;
        }).join('');
    },
    toICAL: function(aValue) {
      return Array.from(aValue).map(function(c) {
        return c === ',' || c === ";" ? '\\' + c : c;
      }).join('');
    }
  }
});

let vcardParams = {
  "type": {
    valueType: "text",
    multiValue: ","
  },
  "value": {
    // since the value here is a 'type' lowercase is used.
    values: ["text", "uri", "date", "time", "date-time", "date-and-or-time",
             "timestamp", "boolean", "integer", "float", "utc-offset",
             "language-tag"],
    allowXName: true,
    allowIanaToken: true
  }
};

let vcardProperties = extend(commonProperties, {
  "adr": { defaultType: "text", structuredValue: ";", multiValue: "," },
  "anniversary": DEFAULT_TYPE_DATE_ANDOR_TIME,
  "bday": DEFAULT_TYPE_DATE_ANDOR_TIME,
  "caladruri": DEFAULT_TYPE_URI,
  "caluri": DEFAULT_TYPE_URI,
  "clientpidmap": DEFAULT_TYPE_TEXT_STRUCTURED,
  "email": DEFAULT_TYPE_TEXT,
  "fburl": DEFAULT_TYPE_URI,
  "fn": DEFAULT_TYPE_TEXT,
  "gender": DEFAULT_TYPE_TEXT_STRUCTURED,
  "geo": DEFAULT_TYPE_URI,
  "impp": DEFAULT_TYPE_URI,
  "key": DEFAULT_TYPE_URI,
  "kind": DEFAULT_TYPE_TEXT,
  "lang": { defaultType: "language-tag" },
  "logo": DEFAULT_TYPE_URI,
  "member": DEFAULT_TYPE_URI,
  "n": { defaultType: "text", structuredValue: ";", multiValue: "," },
  "nickname": DEFAULT_TYPE_TEXT_MULTI,
  "note": DEFAULT_TYPE_TEXT,
  "org": { defaultType: "text", structuredValue: ";" },
  "photo": DEFAULT_TYPE_URI,
  "related": DEFAULT_TYPE_URI,
  "rev": { defaultType: "timestamp" },
  "role": DEFAULT_TYPE_TEXT,
  "sound": DEFAULT_TYPE_URI,
  "source": DEFAULT_TYPE_URI,
  "tel": { defaultType: "uri", allowedTypes: ["uri", "text"] },
  "title": DEFAULT_TYPE_TEXT,
  "tz": { defaultType: "text", allowedTypes: ["text", "utc-offset", "uri"] },
  "xml": DEFAULT_TYPE_TEXT
});

let vcard3Values = extend(commonValues, {
  binary: icalValues.binary,
  date: vcardValues.date,
  "date-time": vcardValues["date-time"],
  "phone-number": vcardValues["phone-number"],
  uri: icalValues.uri,
  text: icalValues.text,
  time: icalValues.time,
  vcard: icalValues.text,
  "utc-offset": {
    toICAL: function(aValue) {
      return aValue.slice(0, 7);
    },

    fromICAL: function(aValue) {
      return aValue.slice(0, 7);
    },

    decorate: function(aValue) {
      return UtcOffset.fromString(aValue);
    },

    undecorate: function(aValue) {
      return aValue.toString();
    }
  }
});

let vcard3Params = {
  "type": {
    valueType: "text",
    multiValue: ","
  },
  "value": {
    // since the value here is a 'type' lowercase is used.
    values: ["text", "uri", "date", "date-time", "phone-number", "time",
             "boolean", "integer", "float", "utc-offset", "vcard", "binary"],
    allowXName: true,
    allowIanaToken: true
  }
};

let vcard3Properties = extend(commonProperties, {
  fn: DEFAULT_TYPE_TEXT,
  n: { defaultType: "text", structuredValue: ";", multiValue: "," },
  nickname: DEFAULT_TYPE_TEXT_MULTI,
  photo: { defaultType: "binary", allowedTypes: ["binary", "uri"] },
  bday: {
    defaultType: "date-time",
    allowedTypes: ["date-time", "date"],
    detectType: function(string) {
      return (string.indexOf('T') === -1) ? 'date' : 'date-time';
    }
  },

  adr: { defaultType: "text", structuredValue: ";", multiValue: "," },
  label: DEFAULT_TYPE_TEXT,

  tel: { defaultType: "phone-number" },
  email: DEFAULT_TYPE_TEXT,
  mailer: DEFAULT_TYPE_TEXT,

  tz: { defaultType: "utc-offset", allowedTypes: ["utc-offset", "text"] },
  geo: { defaultType: "float", structuredValue: ";" },

  title: DEFAULT_TYPE_TEXT,
  role: DEFAULT_TYPE_TEXT,
  logo: { defaultType: "binary", allowedTypes: ["binary", "uri"] },
  agent: { defaultType: "vcard", allowedTypes: ["vcard", "text", "uri"] },
  org: DEFAULT_TYPE_TEXT_STRUCTURED,

  note: DEFAULT_TYPE_TEXT_MULTI,
  prodid: DEFAULT_TYPE_TEXT,
  rev: {
    defaultType: "date-time",
    allowedTypes: ["date-time", "date"],
    detectType: function(string) {
      return (string.indexOf('T') === -1) ? 'date' : 'date-time';
    }
  },
  "sort-string": DEFAULT_TYPE_TEXT,
  sound: { defaultType: "binary", allowedTypes: ["binary", "uri"] },

  class: DEFAULT_TYPE_TEXT,
  key: { defaultType: "binary", allowedTypes: ["binary", "text"] }
});

/**
 * iCalendar design set
 * @type {designSet}
 */
let icalSet = {
  value: icalValues,
  param: icalParams,
  property: icalProperties,
  propertyGroups: false
};

/**
 * vCard 4.0 design set
 * @type {designSet}
 */
let vcardSet = {
  value: vcardValues,
  param: vcardParams,
  property: vcardProperties,
  propertyGroups: true
};

/**
 * vCard 3.0 design set
 * @type {designSet}
 */
let vcard3Set = {
  value: vcard3Values,
  param: vcard3Params,
  property: vcard3Properties,
  propertyGroups: true
};

/**
 * The design data, used by the parser to determine types for properties and
 * other metadata needed to produce correct jCard/jCal data.
 *
 * @alias ICAL.design
 * @exports module:ICAL.design
 */
const design = {
  /**
   * Can be set to false to make the parser more lenient.
   */
  strict: true,

  /**
   * The default set for new properties and components if none is specified.
   * @type {designSet}
   */
  defaultSet: icalSet,

  /**
   * The default type for unknown properties
   * @type {String}
   */
  defaultType: 'unknown',

  /**
   * Holds the design set for known top-level components
   *
   * @type {Object}
   * @property {designSet} vcard       vCard VCARD
   * @property {designSet} vevent      iCalendar VEVENT
   * @property {designSet} vtodo       iCalendar VTODO
   * @property {designSet} vjournal    iCalendar VJOURNAL
   * @property {designSet} valarm      iCalendar VALARM
   * @property {designSet} vtimezone   iCalendar VTIMEZONE
   * @property {designSet} daylight    iCalendar DAYLIGHT
   * @property {designSet} standard    iCalendar STANDARD
   *
   * @example
   * let propertyName = 'fn';
   * let componentDesign = ICAL.design.components.vcard;
   * let propertyDetails = componentDesign.property[propertyName];
   * if (propertyDetails.defaultType == 'text') {
   *   // Yep, sure is...
   * }
   */
  components: {
    vcard: vcardSet,
    vcard3: vcard3Set,
    vevent: icalSet,
    vtodo: icalSet,
    vjournal: icalSet,
    valarm: icalSet,
    vtimezone: icalSet,
    daylight: icalSet,
    standard: icalSet
  },


  /**
   * The design set for iCalendar (rfc5545/rfc7265) components.
   * @type {designSet}
   */
  icalendar: icalSet,

  /**
   * The design set for vCard (rfc6350/rfc7095) components.
   * @type {designSet}
   */
  vcard: vcardSet,

  /**
   * The design set for vCard (rfc2425/rfc2426/rfc7095) components.
   * @type {designSet}
   */
  vcard3: vcard3Set,

  /**
   * Gets the design set for the given component name.
   *
   * @param {String} componentName        The name of the component
   * @return {designSet}      The design set for the component
   */
  getDesignSet: function(componentName) {
    let isInDesign = componentName && componentName in design.components;
    return isInDesign ? design.components[componentName] : design.defaultSet;
  }
};
export default design;