UNPKG

ical.js-one.com

Version:

[![Build Status](https://secure.travis-ci.org/mozilla-comm/ical.js.png?branch=master)](http://travis-ci.org/mozilla-comm/ical.js)

1,875 lines (1,611 loc) 188 kB
(function() { /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. * Portions Copyright (C) Philipp Kewisch, 2011-2012 */ var ICAL = {}; if (typeof(window) === 'undefined') { global.ICAL_previous = ICAL; } else { window.ICAL_previous = ICAL; } ICAL.foldLength = 75; ICAL.newLineChar = '\r\n'; /** * Helper functions used in various places within ical.js */ ICAL.helpers = { initState: function initState(aLine, aLineNr) { return { buffer: aLine, line: aLine, lineNr: aLineNr, character: 0, currentData: null, parentData: [] }; }, initComponentData: function initComponentData(aName) { return { name: aName, type: "COMPONENT", value: [] }; }, /** * Checks if the given number is NaN */ isStrictlyNaN: function(number) { return typeof(number) === 'number' && isNaN(number); }, /** * Parses a string value that is expected to be an * integer, when the valid is not an integer throws * a decoration error. * * @param {String} string raw input. * @return {Number} integer. */ strictParseInt: function(string) { var result = parseInt(string, 10); if (ICAL.helpers.isStrictlyNaN(result)) { throw new Error( 'Could not extract integer from "' + string + '"' ); } return result; }, /** * Creates or returns a class instance * of a given type with the initialization * data if the data is not already an instance * of the given type. * * * Example: * * var time = new ICAL.Time(...); * var result = ICAL.helpers.formatClassType(time, ICAL.Time); * * (result instanceof ICAL.Time) * // => true * * result = ICAL.helpers.formatClassType({}, ICAL.Time); * (result isntanceof ICAL.Time) * // => true * * * @param {Object} data object initialization data. * @param {Object} type object type (like ICAL.Time). */ formatClassType: function formatClassType(data, type) { if (typeof(data) === 'undefined') return undefined; if (data instanceof type) { return data; } return new type(data); }, /** * Identical to index of but will only match values * when they are not preceded by a backslash char \\\ * * @param {String} buffer string value. * @param {String} search value. * @param {Numeric} pos start position. */ unescapedIndexOf: function(buffer, search, pos) { while ((pos = buffer.indexOf(search, pos)) !== -1) { if (pos > 0 && buffer[pos - 1] === '\\') { pos += 1; } else { return pos; } } return -1; }, binsearchInsert: function(list, seekVal, cmpfunc) { if (!list.length) return 0; var low = 0, high = list.length - 1, mid, cmpval; while (low <= high) { mid = low + Math.floor((high - low) / 2); cmpval = cmpfunc(seekVal, list[mid]); if (cmpval < 0) high = mid - 1; else if (cmpval > 0) low = mid + 1; else break; } if (cmpval < 0) return mid; // insertion is displacing, so use mid outright. else if (cmpval > 0) return mid + 1; else return mid; }, dumpn: function() { if (!ICAL.debug) { return null; } if (typeof (console) !== 'undefined' && 'log' in console) { ICAL.helpers.dumpn = function consoleDumpn(input) { return console.log(input); } } else { ICAL.helpers.dumpn = function geckoDumpn(input) { dump(input + '\n'); } } return ICAL.helpers.dumpn(arguments[0]); }, mixin: function(obj, data) { if (data) { for (var k in data) { obj[k] = data[k]; } } return obj; }, isArray: function(o) { return o && (o instanceof Array || typeof o == "array"); }, clone: function(aSrc, aDeep) { if (!aSrc || typeof aSrc != "object") { return aSrc; } else if (aSrc instanceof Date) { return new Date(aSrc.getTime()); } else if ("clone" in aSrc) { return aSrc.clone(); } else if (ICAL.helpers.isArray(aSrc)) { var result = []; for (var i = 0; i < aSrc.length; i++) { result.push(aDeep ? ICAL.helpers.clone(aSrc[i], true) : aSrc[i]); } return result; } else { var result = {}; for (var name in aSrc) { // uses prototype method to allow use of Object.create(null); if (Object.prototype.hasOwnProperty.call(aSrc, name)) { if (aDeep) { result[name] = ICAL.helpers.clone(aSrc[name], true); } else { result[name] = aSrc[name]; } } } return result; } }, unfoldline: function unfoldline(aState) { // Section 3.1 // if the line ends with a CRLF // and the next line starts with a LINEAR WHITESPACE (space, htab, ...) // then remove the CRLF and the whitespace to unsplit the line var moreLines = true; var line = ""; while (moreLines) { moreLines = false; var pos = aState.buffer.search(/\r?\n/); if (pos > -1) { var len = (aState.buffer[pos] == "\r" ? 2 : 1); var nextChar = aState.buffer.substr(pos + len, 1); if (nextChar.match(/^[ \t]$/)) { moreLines = true; line += aState.buffer.substr(0, pos); aState.buffer = aState.buffer.substr(pos + len + 1); } else { // We're at the end of the line, copy the found chunk line += aState.buffer.substr(0, pos); aState.buffer = aState.buffer.substr(pos + len); } } else { line += aState.buffer; aState.buffer = ""; } } return line; }, foldline: function foldline(aLine) { var result = ""; var line = aLine || ""; while (line.length) { result += ICAL.newLineChar + " " + line.substr(0, ICAL.foldLength); line = line.substr(ICAL.foldLength); } return result.substr(ICAL.newLineChar.length + 1); }, ensureKeyExists: function(obj, key, defvalue) { if (!(key in obj)) { obj[key] = defvalue; } }, hasKey: function(obj, key) { return (obj && key in obj && obj[key]); }, pad2: function pad(data) { if (typeof(data) !== 'string') { // handle fractions. if (typeof(data) === 'number') { data = parseInt(data); } data = String(data); } var len = data.length; switch (len) { case 0: return '00'; case 1: return '0' + data; default: return data; } }, trunc: function trunc(number) { return (number < 0 ? Math.ceil(number) : Math.floor(number)); } }; /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. * Portions Copyright (C) Philipp Kewisch, 2011-2012 */ (typeof(ICAL) === 'undefined')? ICAL = {} : ''; ICAL.design = (function() { 'use strict'; var ICAL_NEWLINE = /\\\\|\\;|\\,|\\[Nn]/g; function DecorationError() { Error.apply(this, arguments); } DecorationError.prototype = { __proto__: Error.prototype }; function replaceNewlineReplace(string) { switch (string) { case "\\\\": return "\\"; case "\\;": return ";"; case "\\,": return ","; case "\\n": case "\\N": return "\n"; default: return string; } } function replaceNewline(value) { // avoid regex when possible. if (value.indexOf('\\') === -1) { return value; } return value.replace(ICAL_NEWLINE, replaceNewlineReplace); } /** * Changes the format of the UNTIl part in the RECUR * value type. When no UNTIL part is found the original * is returned untouched. * * @param {String} type toICAL or fromICAL. * @param {String} aValue the value to check. * @return {String} upgraded/original value. */ function recurReplaceUntil(aType, aValue) { var idx = aValue.indexOf('UNTIL='); if (idx === -1) { return aValue; } idx += 6; // everything before the value var begin = aValue.substr(0, idx); // everything after the value var end; // current until value var until; // end of value could be -1 meaning this is the last param. var endValueIdx = aValue.indexOf(';', idx); if (endValueIdx === -1) { end = ''; until = aValue.substr(idx); } else { end = aValue.substr(endValueIdx); until = aValue.substr(idx, endValueIdx - idx); } if (until.length > 10) { until = design.value['date-time'][aType](until); } else { until = design.value.date[aType](until); } return begin + until + end; } /** * Design data used by the parser to decide if data is semantically correct */ var design = { DecorationError: DecorationError, defaultType: 'text', param: { // Although the syntax is DQUOTE uri DQUOTE, I don't think we should // enfoce anything aside from it being a valid content line. // "ALTREP": { ... }, // CN just wants a param-value // "CN": { ... } "cutype": { values: ["INDIVIDUAL", "GROUP", "RESOURCE", "ROOM", "UNKNOWN"], allowXName: true, allowIanaToken: true }, "delegated-from": { valueType: "cal-address", multiValue: "," }, "delegated-to": { valueType: "cal-address", multiValue: "," }, // "DIR": { ... }, // See ALTREP "encoding": { values: ["8BIT", "BASE64"] }, // "FMTTYPE": { ... }, // See ALTREP "fbtype": { values: ["FREE", "BUSY", "BUSY-UNAVAILABLE", "BUSY-TENTATIVE"], allowXName: true, allowIanaToken: true }, // "LANGUAGE": { ... }, // See ALTREP "member": { valueType: "cal-address", multiValue: "," }, "partstat": { // TODO These values are actually different per-component values: ["NEEDS-ACTION", "ACCEPTED", "DECLINED", "TENTATIVE", "DELEGATED", "COMPLETED", "IN-PROCESS"], allowXName: true, allowIanaToken: true }, "range": { values: ["THISLANDFUTURE"] }, "related": { values: ["START", "END"] }, "reltype": { values: ["PARENT", "CHILD", "SIBLING"], allowXName: true, allowIanaToken: true }, "role": { values: ["REQ-PARTICIPANT", "CHAIR", "OPT-PARTICIPANT", "NON-PARTICIPANT"], allowXName: true, allowIanaToken: true }, "rsvp": { valueType: "boolean" }, "sent-by": { valueType: "cal-address" }, "tzid": { matches: /^\// }, "value": { // since the value here is a 'type' lowercase is used. values: ["binary", "boolean", "cal-address", "date", "date-time", "duration", "float", "integer", "period", "recur", "text", "time", "uri", "utc-offset"], allowXName: true, allowIanaToken: true } }, // When adding a value here, be sure to add it to the parameter types! value: { "binary": { decorate: function(aString) { return ICAL.Binary.fromString(aString); }, undecorate: function(aBinary) { return aBinary.toString(); } }, "boolean": { values: ["TRUE", "FALSE"], fromICAL: function(aValue) { switch(aValue) { case 'TRUE': return true; case 'FALSE': return false; default: //TODO: parser warning return false; } }, toICAL: function(aValue) { if (aValue) { return 'TRUE'; } return 'FALSE'; } }, "cal-address": { // needs to be an uri }, "date": { decorate: function(aValue, aProp) { return ICAL.Time.fromDateString(aValue, aProp); }, /** * undecorates a time object. */ undecorate: function(aValue) { return aValue.toString(); }, fromICAL: function(aValue) { // from: 20120901 // to: 2012-09-01 var result = aValue.substr(0, 4) + '-' + aValue.substr(4, 2) + '-' + aValue.substr(6, 2); if (aValue[8] === 'Z') { result += 'Z'; } return result; }, toICAL: function(aValue) { // from: 2012-09-01 // to: 20120901 if (aValue.length > 11) { //TODO: serialize warning? return aValue; } var result = aValue.substr(0, 4) + aValue.substr(5, 2) + aValue.substr(8, 2); if (aValue[10] === 'Z') { result += 'Z'; } return result; } }, "date-time": { fromICAL: function(aValue) { // from: 20120901T130000 // to: 2012-09-01T13:00:00 var result = aValue.substr(0, 4) + '-' + aValue.substr(4, 2) + '-' + aValue.substr(6, 2) + 'T' + aValue.substr(9, 2) + ':' + aValue.substr(11, 2) + ':' + aValue.substr(13, 2); if (aValue[15] === 'Z') { result += 'Z' } return result; }, toICAL: function(aValue) { // from: 2012-09-01T13:00:00 // to: 20120901T130000 if (aValue.length < 19) { // TODO: error return aValue; } var result = aValue.substr(0, 4) + aValue.substr(5, 2) + // grab the (DDTHH) segment aValue.substr(8, 5) + // MM aValue.substr(14, 2) + // SS aValue.substr(17, 2); if (aValue[19] === 'Z') { result += 'Z'; } return result; }, decorate: function(aValue, aProp) { return ICAL.Time.fromDateTimeString(aValue, aProp); }, undecorate: function(aValue) { return aValue.toString(); } }, duration: { decorate: function(aValue) { return ICAL.Duration.fromString(aValue); }, undecorate: function(aValue) { return aValue.toString(); } }, float: { matches: /^[+-]?\d+\.\d+$/, decorate: function(aValue) { return ICAL.Value.fromString(aValue, "float"); }, fromICAL: function(aValue) { var parsed = parseFloat(aValue); if (ICAL.helpers.isStrictlyNaN(parsed)) { // TODO: parser warning return 0.0; } return parsed; }, toICAL: function(aValue) { return String(aValue); } }, integer: { fromICAL: function(aValue) { var parsed = parseInt(aValue); if (ICAL.helpers.isStrictlyNaN(parsed)) { return 0; } return parsed; }, toICAL: function(aValue) { return String(aValue); } }, period: { fromICAL: function(string) { var parts = string.split('/'); var result = design.value['date-time'].fromICAL(parts[0]) + '/'; if (ICAL.Duration.isValueString(parts[1])) { result += parts[1]; } else { result += design.value['date-time'].fromICAL(parts[1]); } return result; }, toICAL: function(string) { var parts = string.split('/'); var result = design.value['date-time'].toICAL(parts[0]) + '/'; if (ICAL.Duration.isValueString(parts[1])) { result += parts[1]; } else { result += design.value['date-time'].toICAL(parts[1]); } return result; }, decorate: function(aValue, aProp) { return ICAL.Period.fromString(aValue, aProp); }, undecorate: function(aValue) { return aValue.toString(); } }, recur: { fromICAL: recurReplaceUntil.bind(this, 'fromICAL'), toICAL: recurReplaceUntil.bind(this, 'toICAL'), decorate: function decorate(aValue) { return ICAL.Recur.fromString(aValue); }, undecorate: function(aRecur) { return aRecur.toString(); } }, text: { matches: /.*/, fromICAL: function(aValue, aName) { return replaceNewline(aValue); }, toICAL: function escape(aValue, aName) { return aValue.replace(/\\|;|,|\n/g, function(str) { switch (str) { case "\\": return "\\\\"; case ";": return "\\;"; case ",": return "\\,"; case "\n": return "\\n"; default: return str; } }); } }, time: { fromICAL: function(aValue) { // from: MMHHSS(Z)? // to: HH:MM:SS(Z)? if (aValue.length < 6) { // TODO: parser exception? return aValue; } // HH::MM::SSZ? var result = aValue.substr(0, 2) + ':' + aValue.substr(2, 2) + ':' + aValue.substr(4, 2); if (aValue[6] === 'Z') { result += 'Z'; } return result; }, toICAL: function(aValue) { // from: HH:MM:SS(Z)? // to: MMHHSS(Z)? if (aValue.length < 8) { //TODO: error return aValue; } var result = aValue.substr(0, 2) + aValue.substr(3, 2) + aValue.substr(6, 2); if (aValue[8] === 'Z') { result += 'Z'; } return result; } }, uri: { // TODO /* ... */ }, "utc-offset": { toICAL: function(aValue) { if (aValue.length < 7) { // no seconds // -0500 return aValue.substr(0, 3) + aValue.substr(4, 2); } else { // seconds // -050000 return aValue.substr(0, 3) + aValue.substr(4, 2) + aValue.substr(7, 2); } }, fromICAL: function(aValue) { if (aValue.length < 6) { // no seconds // -05:00 return aValue.substr(0, 3) + ':' + aValue.substr(3, 2); } else { // seconds // -05:00:00 return aValue.substr(0, 3) + ':' + aValue.substr(3, 2) + ':' + aValue.substr(5, 2); } }, decorate: function(aValue) { return ICAL.UtcOffset.fromString(aValue); }, undecorate: function(aValue) { return aValue.toString(); } } }, property: { decorate: function decorate(aData, aParent) { return new ICAL.Property(aData, aParent); }, "attach": { defaultType: "uri" }, "attendee": { defaultType: "cal-address" }, "categories": { defaultType: "text", multiValue: "," }, "completed": { defaultType: "date-time" }, "created": { defaultType: "date-time" }, "dtend": { defaultType: "date-time", allowedTypes: ["date-time", "date"] }, "dtstamp": { defaultType: "date-time" }, "dtstart": { defaultType: "date-time", allowedTypes: ["date-time", "date"] }, "due": { defaultType: "date-time", allowedTypes: ["date-time", "date"] }, "duration": { defaultType: "duration" }, "exdate": { defaultType: "date-time", allowedTypes: ["date-time", "date"], multiValue: ',' }, "exrule": { defaultType: "recur" }, "freebusy": { defaultType: "period", multiValue: "," }, "geo": { defaultType: "float", multiValue: ";" }, /* TODO exactly 2 values */"last-modified": { defaultType: "date-time" }, "organizer": { defaultType: "cal-address" }, "percent-complete": { defaultType: "integer" }, "repeat": { defaultType: "integer" }, "rdate": { defaultType: "date-time", allowedTypes: ["date-time", "date", "period"], multiValue: ',', detectType: function(string) { if (string.indexOf('/') !== -1) { return 'period'; } return (string.indexOf('T') === -1) ? 'date' : 'date-time'; } }, "recurrence-id": { defaultType: "date-time", allowedTypes: ["date-time", "date"] }, "resources": { defaultType: "text", multiValue: "," }, "request-status": { defaultType: "text", multiValue: ";" }, "priority": { defaultType: "integer" }, "rrule": { defaultType: "recur" }, "sequence": { defaultType: "integer" }, "trigger": { defaultType: "duration", allowedTypes: ["duration", "date-time"] }, "tzoffsetfrom": { defaultType: "utc-offset" }, "tzoffsetto": { defaultType: "utc-offset" }, "tzurl": { defaultType: "uri" }, "url": { defaultType: "uri" } }, component: { decorate: function decorate(aData, aParent) { return new ICAL.Component(aData, aParent); }, "vevent": {} } }; return design; }()); ICAL.stringify = (function() { 'use strict'; var LINE_ENDING = '\r\n'; var DEFAULT_TYPE = 'text'; var design = ICAL.design; var helpers = ICAL.helpers; /** * Convert a full jCal Array into a ical document. * * @param {Array} jCal document. * @return {String} ical document. */ function stringify(jCal) { if (!jCal[0] || jCal[0] !== 'icalendar') { throw new Error('must provide full jCal document'); } // 1 because we skip the initial element. var i = 1; var len = jCal.length; var result = ''; for (; i < len; i++) { result += stringify.component(jCal[i]) + LINE_ENDING; } return result; } /** * Converts an jCal component array into a ICAL string. * Recursive will resolve sub-components. * * Exact component/property order is not saved all * properties will come before subcomponents. * * @param {Array} component jCal fragment of a component. */ stringify.component = function(component) { var name = component[0].toUpperCase(); var result = 'BEGIN:' + name + LINE_ENDING; var props = component[1]; var propIdx = 0; var propLen = props.length; for (; propIdx < propLen; propIdx++) { result += stringify.property(props[propIdx]) + LINE_ENDING; } var comps = component[2]; var compIdx = 0; var compLen = comps.length; for (; compIdx < compLen; compIdx++) { result += stringify.component(comps[compIdx]) + LINE_ENDING; } result += 'END:' + name; return result; } /** * Converts a single property to a ICAL string. * * @param {Array} property jCal property. */ stringify.property = function(property) { var name = property[0].toUpperCase(); var jsName = property[0]; var params = property[1]; var line = name; var paramName; for (paramName in params) { if (params.hasOwnProperty(paramName)) { line += ';' + paramName.toUpperCase(); line += '=' + stringify.propertyValue(params[paramName]); } } // there is no value so return. if (property.length === 3) { // if no params where inserted and no value // we given we must add a blank value. if (!paramName) { line += ':'; } return line; } var valueType = property[2]; var propDetails; var multiValue = false; var isDefault = false; if (jsName in design.property) { propDetails = design.property[jsName]; if ('multiValue' in propDetails) { multiValue = propDetails.multiValue; } if ('defaultType' in propDetails) { if (valueType === propDetails.defaultType) { isDefault = true; } } else { if (valueType === DEFAULT_TYPE) { isDefault = true; } } } else { if (valueType === DEFAULT_TYPE) { isDefault = true; } } // push the VALUE property if type is not the default // for the current property. if (!isDefault) { // value will never contain ;/:/, so we don't escape it here. line += ';VALUE=' + valueType.toUpperCase(); } line += ':'; if (multiValue) { line += stringify.multiValue( property.slice(3), multiValue, valueType ); } else { line += stringify.value(property[3], valueType); } return ICAL.helpers.foldline(line); } /** * Handles escaping of property values that may contain: * * COLON (:), SEMICOLON (;), or COMMA (,) * * If any of the above are present the result is wrapped * in double quotes. * * @param {String} value raw value. * @return {String} given or escaped value when needed. */ stringify.propertyValue = function(value) { if ((helpers.unescapedIndexOf(value, ',') === -1) && (helpers.unescapedIndexOf(value, ':') === -1) && (helpers.unescapedIndexOf(value, ';') === -1)) { return value; } return '"' + value + '"'; } /** * Converts an array of ical values into a single * string based on a type and a delimiter value (like ","). * * @param {Array} values list of values to convert. * @param {String} delim used to join the values usually (",", ";", ":"). * @param {String} type lowecase ical value type * (like boolean, date-time, etc..). * * @return {String} ical string for value. */ stringify.multiValue = function(values, delim, type) { var result = ''; var len = values.length; var i = 0; for (; i < len; i++) { result += stringify.value(values[i], type); if (i !== (len - 1)) { result += delim; } } return result; } /** * Processes a single ical value runs the associated "toICAL" * method from the design value type if available to convert * the value. * * @param {String|Numeric} value some formatted value. * @param {String} type lowecase ical value type * (like boolean, date-time, etc..). * @return {String} ical value for single value. */ stringify.value = function(value, type) { if (type in design.value && 'toICAL' in design.value[type]) { return design.value[type].toICAL(value); } return value; } return stringify; }()); ICAL.parse = (function() { 'use strict'; var CHAR = /[^ \t]/; var MULTIVALUE_DELIMITER = ','; var VALUE_DELIMITER = ':'; var PARAM_DELIMITER = ';'; var PARAM_NAME_DELIMITER = '='; var DEFAULT_TYPE = 'text'; var design = ICAL.design; var helpers = ICAL.helpers; function ParserError(message) { this.message = message; try { throw new Error(); } catch (e) { var split = e.stack.split('\n'); split.shift(); this.stack = split.join('\n'); } } ParserError.prototype = { __proto__: Error.prototype }; function parser(input) { var state = {}; var root = state.component = [ 'icalendar' ]; state.stack = [root]; parser._eachLine(input, function(err, line) { parser._handleContentLine(line, state); }); // when there are still items on the stack // throw a fatal error, a component was not closed // correctly in that case. if (state.stack.length > 1) { throw new ParserError( 'invalid ical body. component began but did not end' ); } state = null; return root; } // classes & constants parser.ParserError = ParserError; parser._formatName = function(name) { return name.toLowerCase(); } parser._handleContentLine = function(line, state) { // break up the parts of the line var valuePos = line.indexOf(VALUE_DELIMITER); var paramPos = line.indexOf(PARAM_DELIMITER); var nextPos = 0; // name of property or begin/end var name; var value; var params; /** * Different property cases * * * 1. RRULE:FREQ=foo * // FREQ= is not a param but the value * * 2. ATTENDEE;ROLE=REQ-PARTICIPANT; * // ROLE= is a param because : has not happened yet */ if ((paramPos !== -1 && valuePos !== -1)) { // when the parameter delimiter is after the // value delimiter then its not a parameter. if (paramPos > valuePos) { paramPos = -1; } } if (paramPos !== -1) { // when there are parameters (ATTENDEE;RSVP=TRUE;) name = parser._formatName(line.substr(0, paramPos)); params = parser._parseParameters(line, paramPos); if (valuePos !== -1) { value = line.substr(valuePos + 1); } } else if (valuePos !== -1) { // without parmeters (BEGIN:VCAENDAR, CLASS:PUBLIC) name = parser._formatName(line.substr(0, valuePos)); value = line.substr(valuePos + 1); if (name === 'begin') { var newComponent = [parser._formatName(value), [], []]; if (state.stack.length === 1) { state.component.push(newComponent); } else { state.component[2].push(newComponent); } state.stack.push(state.component); state.component = newComponent; return; } if (name === 'end') { state.component = state.stack.pop(); return; } } else { /** * Invalid line. * The rational to throw an error is we will * never be certain that the rest of the file * is sane and its unlikely that we can serialize * the result correctly either. */ throw new ParserError( 'invalid line (no token ";" or ":") "' + line + '"' ); } var valueType; var multiValue = false; var propertyDetails; if (name in design.property) { propertyDetails = design.property[name]; if ('multiValue' in propertyDetails) { multiValue = propertyDetails.multiValue; } if (value && 'detectType' in propertyDetails) { valueType = propertyDetails.detectType(value); } } // at this point params is mandatory per jcal spec params = params || {}; // attempt to determine value if (!valueType) { if (!('value' in params)) { if (propertyDetails) { valueType = propertyDetails.defaultType; } else { valueType = DEFAULT_TYPE; } } else { // possible to avoid this? valueType = params.value.toLowerCase(); } } delete params.value; /** * Note on `var result` juggling: * * I observed that building the array in pieces has adverse * effects on performance, so where possible we inline the creation. * Its a little ugly but resulted in ~2000 additional ops/sec. */ if (value) { if (multiValue) { var result = [name, params, valueType]; parser._parseMultiValue(value, multiValue, valueType, result); } else { value = parser._parseValue(value, valueType); var result = [name, params, valueType, value]; } } else { var result = [name, params, valueType]; } state.component[1].push(result); }; /** * @param {String} value original value. * @param {String} type type of value. * @return {Object} varies on type. */ parser._parseValue = function(value, type) { if (type in design.value && 'fromICAL' in design.value[type]) { return design.value[type].fromICAL(value); } return value; }; /** * Parse parameters from a string to object. * * @param {String} line a single unfolded line. * @param {Numeric} start position to start looking for properties. * @param {Numeric} maxPos position at which values start. * @return {Object} key/value pairs. */ parser._parseParameters = function(line, start) { var lastParam = start; var pos = 0; var delim = PARAM_NAME_DELIMITER; var result = {}; // find the next '=' sign // use lastParam and pos to find name // check if " is used if so get value from "->" // then increment pos to find next ; while ((pos !== false) && (pos = helpers.unescapedIndexOf(line, delim, pos + 1)) !== -1) { var name = line.substr(lastParam + 1, pos - lastParam - 1); var nextChar = line[pos + 1]; var substrOffset = -2; if (nextChar === '"') { var valuePos = pos + 2; pos = helpers.unescapedIndexOf(line, '"', valuePos); var value = line.substr(valuePos, pos - valuePos); lastParam = helpers.unescapedIndexOf(line, PARAM_DELIMITER, pos); } else { var valuePos = pos + 1; substrOffset = -1; // move to next ";" var nextPos = helpers.unescapedIndexOf(line, PARAM_DELIMITER, valuePos); if (nextPos === -1) { // when there is no ";" attempt to locate ":" nextPos = helpers.unescapedIndexOf(line, VALUE_DELIMITER, valuePos); // no more tokens end of the line use .length if (nextPos === -1) { nextPos = line.length; // because we are at the end we don't need to trim // the found value of substr offset is zero substrOffset = 0; } else { // next token is the beginning of the value // so we must stop looking for the '=' token. pos = false; } } else { lastParam = nextPos; } var value = line.substr(valuePos, nextPos - valuePos); } var type = DEFAULT_TYPE; if (name in design.param && design.param[name].valueType) { type = design.param[name].valueType; } result[parser._formatName(name)] = parser._parseValue(value, type); } return result; } /** * Parse a multi value string */ parser._parseMultiValue = function(buffer, delim, type, result) { var pos = 0; var lastPos = 0; // split each piece while ((pos = helpers.unescapedIndexOf(buffer, delim, lastPos)) !== -1) { var value = buffer.substr(lastPos, pos - lastPos); result.push(parser._parseValue(value, type)); lastPos = pos + 1; } // on the last piece take the rest of string result.push( parser._parseValue(buffer.substr(lastPos), type) ); return result; } parser._eachLine = function(buffer, callback) { var len = buffer.length; var lastPos = buffer.search(CHAR); var pos = lastPos; var line; var firstChar; var newlineOffset; do { pos = buffer.indexOf('\n', lastPos) + 1; if (buffer[pos - 2] === '\r') { newlineOffset = 2; } else { newlineOffset = 1; } if (pos === 0) { pos = len; newlineOffset = 0; } firstChar = buffer[lastPos]; if (firstChar === ' ' || firstChar === '\t') { // add to line line += buffer.substr( lastPos + 1, pos - lastPos - (newlineOffset + 1) ); } else { if (line) callback(null, line); // push line line = buffer.substr( lastPos, pos - lastPos - newlineOffset ); } lastPos = pos; } while (pos !== len); // extra ending line line = line.trim(); if (line.length) callback(null, line); } return parser; }()); ICAL.Component = (function() { 'use strict'; var PROPERTY_INDEX = 1; var COMPONENT_INDEX = 2; var NAME_INDEX = 0; /** * Create a wrapper for a jCal component. * * @param {Array|String} jCal * raw jCal component data OR name of new component. * @param {ICAL.Component} parent parent component to associate. */ function Component(jCal, parent) { if (typeof(jCal) === 'string') { // jCal spec (name, properties, components) jCal = [jCal, [], []]; } // mostly for legacy reasons. this.jCal = jCal; if (parent) { this.parent = parent; } } Component.prototype = { /** * Hydrated properties are inserted into the _properties array at the same * position as in the jCal array, so its possible the array contains * undefined values for unhydrdated properties. To avoid iterating the * array when checking if all properties have been hydrated, we save the * count here. */ _hydratedPropertyCount: 0, /** * The same count as for _hydratedPropertyCount, but for subcomponents */ _hydratedComponentCount: 0, get name() { return this.jCal[NAME_INDEX]; }, _hydrateComponent: function(index) { if (!this._components) { this._components = []; this._hydratedComponentCount = 0; } if (this._components[index]) { return this._components[index]; } var comp = new Component( this.jCal[COMPONENT_INDEX][index], this ); this._hydratedComponentCount++; return this._components[index] = comp; }, _hydrateProperty: function(index) { if (!this._properties) { this._properties = []; this._hydratedPropertyCount = 0; } if (this._properties[index]) { return this._properties[index]; } var prop = new ICAL.Property( this.jCal[PROPERTY_INDEX][index], this ); this._hydratedPropertyCount++; return this._properties[index] = prop; }, /** * Finds first sub component, optionally filtered by name. * * @method getFirstSubcomponent * @param {String} [name] optional name to filter by. */ getFirstSubcomponent: function(name) { if (name) { var i = 0; var comps = this.jCal[COMPONENT_INDEX]; var len = comps.length; for (; i < len; i++) { if (comps[i][NAME_INDEX] === name) { var result = this._hydrateComponent(i); return result; } } } else { if (this.jCal[COMPONENT_INDEX].length) { return this._hydrateComponent(0); } } // ensure we return a value (strict mode) return null; }, /** * Finds all sub components, optionally filtering by name. * * @method getAllSubcomponents * @param {String} [name] optional name to filter by. */ getAllSubcomponents: function(name) { var jCalLen = this.jCal[COMPONENT_INDEX].length; if (name) { var comps = this.jCal[COMPONENT_INDEX]; var result = []; var i = 0; for (; i < jCalLen; i++) { if (name === comps[i][NAME_INDEX]) { result.push( this._hydrateComponent(i) ); } } return result; } else { if (!this._components || (this._hydratedComponentCount !== jCalLen)) { var i = 0; for (; i < jCalLen; i++) { this._hydrateComponent(i); } } return this._components; } }, /** * Returns true when a named property exists. * * @param {String} name property name. * @return {Boolean} true when property is found. */ hasProperty: function(name) { var props = this.jCal[PROPERTY_INDEX]; var len = props.length; var i = 0; for (; i < len; i++) { // 0 is property name if (props[i][NAME_INDEX] === name) { return true; } } return false; }, /** * Finds first property. * * @param {String} [name] lowercase name of property. * @return {ICAL.Property} found property. */ getFirstProperty: function(name) { if (name) { var i = 0; var props = this.jCal[PROPERTY_INDEX]; var len = props.length; for (; i < len; i++) { if (props[i][NAME_INDEX] === name) { var result = this._hydrateProperty(i); return result; } } } else { if (this.jCal[PROPERTY_INDEX].length) { return this._hydrateProperty(0); } } return null; }, /** * Returns first properties value if available. * * @param {String} [name] (lowecase) property name. * @return {String} property value. */ getFirstPropertyValue: function(name) { var prop = this.getFirstProperty(name); if (prop) { return prop.getFirstValue(); } return null; }, /** * get all properties in the component. * * @param {String} [name] (lowercase) property name. * @return {Array[ICAL.Property]} list of properties. */ getAllProperties: function(name) { var jCalLen = this.jCal[PROPERTY_INDEX].length; if (name) { var props = this.jCal[PROPERTY_INDEX]; var result = []; var i = 0; for (; i < jCalLen; i++) { if (name === props[i][NAME_INDEX]) { result.push( this._hydrateProperty(i) ); } } return result; } else { if (!this._properties || (this._hydratedPropertyCount !== jCalLen)) { var i = 0; for (; i < jCalLen; i++) { this._hydrateProperty(i); } } return this._properties; } return null; }, _removeObjectByIndex: function(jCalIndex, cache, index) { // remove cached version if (cache && cache[index]) { cache.splice(index, 1); } // remove it from the jCal this.jCal[jCalIndex].splice(index, 1); }, _removeObject: function(jCalIndex, cache, nameOrObject) { var i = 0; var objects = this.jCal[jCalIndex]; var len = objects.length; var cached = this[cache]; if (typeof(nameOrObject) === 'string') { for (; i < len; i++) { if (objects[i][NAME_INDEX] === nameOrObject) { this._removeObjectByIndex(jCalIndex, cached, i); return true; } } } else if (cached) { for (; i < len; i++) { if (cached[i] && cached[i] === nameOrObject) { this._removeObjectByIndex(jCalIndex, cached, i); return true; } } } return false; }, _removeAllObjects: function(jCalIndex, cache, name) { var cached = this[cache]; if (name) { var objects = this.jCal[jCalIndex]; var i = objects.length - 1; // descending search required because splice // is used and will effect the indices. for (; i >= 0; i--) { if (objects[i][NAME_INDEX] === name) { this._removeObjectByIndex(jCalIndex, cached, i); } } } else { if (cache in this) { // I think its probable that when we remove all // of a type we may want to add to it again so it // makes sense to reuse the object in that case. // For now we remove the contents of the array. this[cache].length = 0; } this.jCal[jCalIndex].length = 0; } }, /** * Adds a single sub component. * * @param {ICAL.Component} component to add. */ addSubcomponent: function(component) { if (!this._components) { this._components = []; this._hydratedComponentCount = 0; } var idx = this.jCal[COMPONENT_INDEX].push(component.jCal); this._components[idx - 1] = component; this._hydratedComponentCount++; }, /** * Removes a single component by name or * the instance of a specific component. * * @param {ICAL.Component|String} nameOrComp comp type. * @return {Boolean} true when comp is removed. */ removeSubcomponent: function(nameOrComp) { var removed = this._removeObject(COMPONENT_INDEX, '_components', nameOrComp); if (removed) { this._hydratedComponentCount--; } return removed; }, /** * Removes all components or (if given) all * components by a particular name. * * @param {String} [name] (lowercase) component name. */ removeAllSubcomponents: function(name) { var removed = this._removeAllObjects(COMPONENT_INDEX, '_components', name); this._hydratedComponentCount = 0; return removed; }, /** * Adds a property to the component. * * @param {ICAL.Property} property object. */ addProperty: function(property) { if (!(property instanceof ICAL.Property)) { throw new TypeError('must instance of ICAL.Property'); } var idx = this.jCal[PROPERTY_INDEX].push(property.jCal); property.component = this; if (!this._properties) { this._properties = []; this._hydratedPropertyCount = 0; } this._properties[idx - 1] = property; this._hydratedPropertyCount++; }, /** * Helper method to add a property with a value to the component. * * @param {String} name property name to add. * @param {Object} value property value. */ addPropertyWithValue: function(name, value) { var prop = new ICAL.Property(name, this); prop.setValue(value);