From 90090182c63617015830116dd4100136ed8c6b8b Mon Sep 17 00:00:00 2001 From: James Lal Date: Thu, 6 Dec 2012 20:26:05 -0800 Subject: new ical parser --- test/support/ical.js | 3143 ++++++++++++++++++++++++++++++-------------------- 1 file changed, 1913 insertions(+), 1230 deletions(-) diff --git a/test/support/ical.js b/test/support/ical.js index 8ffd57c..519e89e 100644 --- a/test/support/ical.js +++ b/test/support/ical.js @@ -6,6 +6,9 @@ if (typeof(ICAL) === 'undefined') (typeof(window) !== 'undefined') ? this.ICAL = {} : ICAL = {}; +ICAL.foldLength = 75; +ICAL.newLineChar = '\r\n'; + /** * Helper functions used in various places within ical.js */ @@ -62,6 +65,25 @@ ICAL.helpers = { 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; @@ -203,337 +225,1278 @@ ICAL.helpers = { }, pad2: function pad(data) { - return ("00" + data).substr(-2); + if (typeof(data) !== 'string') { + 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 = {} : ''; -(function() { - ICAL.serializer = { - serializeToIcal: function(obj, name, isParam) { - if (obj && obj.icalclass) { - return obj.toString(); +ICAL.design = (function() { + 'use strict'; + + var ICAL_NEWLINE = /\\\\|\\;|\\,|\\[Nn]/g; + + function DecorationError() { + Error.apply(this, arguments); + } + + DecorationError.prototype = { + __proto__: Error.prototype + }; + + function isStrictlyNaN(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. + */ + function strictParseInt(string) { + var result = parseInt(string, 10); + + if (isStrictlyNaN(result)) { + throw new DecorationError( + 'Could not extract integer from "' + string + '"' + ); + } + + return result; + } + + 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); + } + + /** + * 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: { - var str = ""; + "binary": { + decorate: function(aString) { + return ICAL.icalbinary.fromString(aString); + }, - if (obj.type == "COMPONENT") { - str = "BEGIN:" + obj.name + ICAL.newLineChar; - for (var subkey in obj.value) { - str += this.serializeToIcal(obj.value[subkey]) + ICAL.newLineChar; + 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": { + validate: function(aValue) { + var state = { + buffer: aValue + }; + var data = ICAL.DecorationParser.parseDate(state); + ICAL.DecorationParser.expectEnd(state, "Junk at end of DATE value"); + return data; + }, + + decorate: function(aValue) { + // YYYY-MM-DD + // 2012-10-10 + return new ICAL.icaltime({ + year: strictParseInt(aValue.substr(0, 4)), + month: strictParseInt(aValue.substr(5, 2)), + day: strictParseInt(aValue.substr(8, 2)), + isDate: true + }); + }, + + /** + * undecorates a time object. + */ + undecorate: function(aValue) { + // 2012-10-10 + return aValue.year + '-' + + ICAL.helpers.pad2(aValue.month) + '-' + + ICAL.helpers.pad2(aValue.day); + }, + + fromICAL: function(aValue) { + // from: 20120901 + // to: 2012-09-01 + var result = aValue.substr(0, 4) + '-' + + aValue.substr(4, 2) + '-' + + aValue.substr(6, 2); + + return result; + }, + + toICAL: function(aValue) { + // from: 2012-09-01 + // to: 20120901 + + if (aValue.length !== 10) { + //TODO: serialize warning? + return aValue; + } + + return aValue.substr(0, 4) + + aValue.substr(5, 2) + + aValue.substr(8, 2); + } + }, + "date-time": { + validate: function(aValue) { + var state = { + buffer: aValue + }; + var data = ICAL.DecorationParser.parseDateTime(state); + ICAL.DecorationParser.expectEnd(state, "Junk at end of DATE-TIME value"); + return data; + }, + + 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) { + if (aValue.length < 19) { + throw new DecorationError( + 'invalid date-time value: "' + aValue + '"' + ); + } + + // 2012-10-10T10:10:10(Z)? + var time = new ICAL.icaltime({ + year: strictParseInt(aValue.substr(0, 4)), + month: strictParseInt(aValue.substr(5, 2)), + day: strictParseInt(aValue.substr(8, 2)), + hour: strictParseInt(aValue.substr(11, 2)), + minute: strictParseInt(aValue.substr(14, 2)), + second: strictParseInt(aValue.substr(17, 2)) + }); + + if (aValue[19] === 'Z') { + time.zone = ICAL.icaltimezone.utc_timezone; + } + + return time; + }, + + undecorate: function(aValue) { + var result = aValue.year + '-' + + ICAL.helpers.pad2(aValue.month) + '-' + + ICAL.helpers.pad2(aValue.day) + 'T' + + ICAL.helpers.pad2(aValue.hour) + ':' + + ICAL.helpers.pad2(aValue.minute) + ':' + + ICAL.helpers.pad2(aValue.second); + + if (aValue.zone === ICAL.icaltimezone.utc_timezone) { + result += 'Z'; + } + + return result; + } + }, + duration: { + decorate: function(aValue) { + return ICAL.icalduration.fromString(aValue); + }, + undecorate: function(aValue) { + return aValue.toString(); + } + }, + float: { + matches: /^[+-]?\d+\.\d+$/, + decorate: function(aValue) { + return ICAL.icalvalue.fromString(aValue, "float"); + }, + + fromICAL: function(aValue) { + var parsed = parseFloat(aValue); + if (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 (isStrictlyNaN(parsed)) { + return 0; + } + return parsed; + }, + + toICAL: function(aValue) { + return String(aValue); + } + }, + period: { + validate: function(aValue) { + var state = { + buffer: aValue + }; + var data = ICAL.DecorationParser.parsePeriod(state); + ICAL.DecorationParser.expectEnd(state, "Junk at end of PERIOD value"); + return data; + }, + + decorate: function(aValue) { + return ICAL.icalperiod.fromString(aValue); + }, + + undecorate: function(aValue) { + return aValue.toString(); + } + }, + recur: { + validate: function(aValue) { + var state = { + buffer: aValue + }; + var data = ICAL.DecorationParser.parseRecur(state); + ICAL.DecorationParser.expectEnd(state, "Junk at end of RECUR value"); + return data; + }, + + decorate: function decorate(aValue) { + return ICAL.icalrecur.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: { + validate: function(aValue) { + var state = { + buffer: aValue + }; + var data = ICAL.DecorationParser.parseTime(state); + ICAL.DecorationParser.expectEnd(state, "Junk at end of TIME value"); + return data; + }, + + 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": { + validate: function(aValue) { + var state = { + buffer: aValue + }; + var data = ICAL.DecorationParser.parseUtcOffset(state); + ICAL.DecorationParser.expectEnd(state, "Junk at end of UTC-OFFSET value"); + return data; + }, + + decorate: function(aValue) { + return ICAL.icalutcoffset.fromString(aValue); + }, + + undecorate: function(aValue) { + return aValue.toString(); } - str += "END:" + obj.name; - } else { - str += ICAL.icalparser.stringifyProperty(obj); } - return str; + }, + + 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: ',' + }, + "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; }()); -/* 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 */ +ICAL.stringify = (function() { + 'use strict'; -// TODO validate known parameters -// TODO make sure all known types don't contain junk -// TODO tests for parsers -// TODO SAX type parser -// TODO structure data in components -// TODO enforce uppercase when parsing -// TODO optionally preserve value types that are default but explicitly set -// TODO floating timezone -(typeof(ICAL) === 'undefined')? ICAL = {} : ''; -(function() { - /* NOTE: I'm not sure this is the latest syntax... - - { - X-WR-CALNAME: "test", - components: { - VTIMEZONE: { ... }, - VEVENT: { - "uuid1": { - UID: "uuid1", - ... - components: { - VALARM: [ - ... - ] - } - } - }, - VTODO: { ... } - } - } - */ + var LINE_ENDING = '\r\n'; + var DEFAULT_TYPE = 'text'; - // Exports + var design = ICAL.design; + var helpers = ICAL.helpers; - function ParserError(aState, aMessage) { - this.mState = aState; - this.name = "ParserError"; - if (aState) { - var lineNrData = ("lineNr" in aState ? aState.lineNr + ":" : "") + - ("character" in aState && !isNaN(aState.character) ? - aState.character + ":" : - ""); + /** + * 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'); + } - var message = lineNrData + aMessage; - if ("buffer" in aState) { - if (aState.buffer) { - message += " before '" + aState.buffer + "'"; - } else { - message += " at end of line"; - } + // 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]); } - if ("line" in aState) { - message += " in '" + aState.line + "'"; + } + + // 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; + } } - this.message = message; } else { - this.message = aMessage; + if (valueType === DEFAULT_TYPE) { + isDefault = true; + } } - // create stack - try { - throw new Error(); - } catch (e) { - var split = e.stack.split('\n'); - split.shift(); - this.stack = split.join('\n'); + // 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); } - ParserError.prototype = { - __proto__: Error.prototype, - constructor: ParserError - }; + /** + * 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) { - var parser = { - Error: ParserError - }; - ICAL.icalparser = parser; + if ((helpers.unescapedIndexOf(value, ',') === -1) && + (helpers.unescapedIndexOf(value, ':') === -1) && + (helpers.unescapedIndexOf(value, ';') === -1)) { + return value; + } - parser.lexContentLine = function lexContentLine(aState) { - // contentline = name *(";" param ) ":" value CRLF - // The corresponding json object will be: - // { name: "name", parameters: { key: "value" }, value: "value" } - var lineData = {}; + return '"' + value + '"'; + } - // Parse the name - lineData.name = parser.lexName(aState); + /** + * 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; - // Read Paramaters, if there are any. - if (aState.buffer.substr(0, 1) == ";") { - lineData.parameters = {}; - while (aState.buffer.substr(0, 1) == ";") { - aState.buffer = aState.buffer.substr(1); - var param = parser.lexParam(aState); - lineData.parameters[param.name] = param.value; + for (; i < len; i++) { + result += stringify.value(values[i], type); + if (i !== (len - 1)) { + result += delim; } } - // Read the value - parser.expectRE(aState, /^:/, "Expected ':'"); - lineData.value = parser.lexValue(aState); - parser.expectEnd(aState, "Junk at End of Line"); - return lineData; - }; + return result; + } - parser.lexName = function lexName(aState) { - function parseIanaToken(aState) { - var match = parser.expectRE(aState, /^([A-Za-z0-9-]+)/, - "Expected IANA Token"); - return match[1]; + /** + * 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; + } - function parseXName(aState) { - var error = "Expected XName"; - var value = "X-"; - var match = parser.expectRE(aState, /^X-/, error); + return stringify; - // Vendor ID - if ((match = parser.expectOptionalRE(aState, /^([A-Za-z0-9]+-)/, error))) { - value += match[1]; - } +}()); - // Remaining part - match = parser.expectRE(aState, /^([A-Za-z0-9-]+)/, error); - value += match[1]; +ICAL.parse = (function() { + 'use strict'; - return value; - } - return parser.parseAlternative(aState, parseXName, parseIanaToken); + 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) { + Error.apply(this, arguments); + } + + ParserError.prototype = { + __proto__: Error.prototype }; - parser.lexValue = function lexValue(aState) { - // VALUE-CHAR = WSP / %x21-7E / NON-US-ASCII - // ; Any textual character + function parser(input) { + var state = {}; + var root = state.component = [ + 'icalendar' + ]; + + state.stack = [root]; + + parser._eachLine(input, function(err, line) { + parser._handleContentLine(line, state); + }); - if (aState.buffer.length === 0) { - return aState.buffer; + + // 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, a began started but did not end' + ); } - // TODO the unicode range might be wrong! - var match = parser.expectRE(aState, - /* WSP|%x21-7E|NON-US-ASCII */ - /^([ \t\x21-\x7E\u00C2-\uF400]+)/, - "Invalid Character in value"); + state = null; - return match[1]; - }; + return root; + } - parser.lexParam = function lexParam(aState) { - // read param name - var name = parser.lexName(aState); - parser.expectRE(aState, /^=/, "Expected '='"); + // classes & constants + parser.ParserError = ParserError; - // read param value - var values = parser.parseList(aState, parser.lexParamValue, ","); - return { - name: name, - value: (values.length == 1 ? values[0] : values) - }; - }; + parser._formatName = function(name) { + return name.toLowerCase(); + } - parser.lexParamValue = function lexParamValue(aState) { - // CONTROL = %x00-08 / %x0A-1F / %x7F - // ; All the controls except HTAB - function parseQuotedString(aState) { - parser.expectRE(aState, /^"/, "Expecting Quote Character"); - // QSAFE-CHAR = WSP / %x21 / %x23-7E / NON-US-ASCII - // ; Any character except CONTROL and DQUOTE - - var match = parser.expectRE(aState, /^([^"\x00-\x08\x0A-\x1F\x7F]*)/, - "Invalid Param Value"); - parser.expectRE(aState, /^"/, "Expecting Quote Character"); - return match[1]; + 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; + } } - function lexParamText(aState) { - // SAFE-CHAR = WSP / %x21 / %x23-2B / %x2D-39 / %x3C-7E / NON-US-ASCII - // ; Any character except CONTROL, DQUOTE, ";", ":", "," - var match = parser.expectRE(aState, /^([^";:,\x00-\x08\x0A-\x1F\x7F]*)/, - "Invalid Param Value"); - return match[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 + '"' + ); } - return parser.parseAlternative(aState, parseQuotedString, lexParamText); - }; + var valueType; + var multiValue = false; + var propertyDetails; - parser.parseContentLine = function parseContentLine(aState, aLineData) { - - switch (aLineData.name) { - case "BEGIN": - var newdata = ICAL.helpers.initComponentData(aLineData.value); - if (aState.currentData) { - // If there is already data (i.e this is not the top level - // component), then push the new data to its values and - // stack the parent data. - aState.currentData.value.push(newdata); - aState.parentData.push(aState.currentData); - } - - aState.currentData = newdata; // set the new data array - break; - case "END": - if (aState.currentData.name != aLineData.value) { - throw new ParserError(aState, "Unexpected END:" + aLineData.value + - ", expected END:" + aState.currentData.name); - } - if (aState.parentData.length) { - aState.currentData = aState.parentData.pop(); - } - break; - default: - ICAL.helpers.dumpn("parse " + aLineData.toString()); - parser.detectParameterType(aLineData); - parser.detectValueType(aLineData); - ICAL.helpers.dumpn("parse " + aLineData.toString()); - aState.currentData.value.push(aLineData); - break; + if (name in design.property) { + propertyDetails = design.property[name]; + + if ('multiValue' in propertyDetails) { + multiValue = propertyDetails.multiValue; + } } - }, - parser.detectParameterType = function detectParameterType(aLineData) { - for (var name in aLineData.parameters) { - var paramType = "TEXT"; + // at this point params is mandatory per jcal spec + params = params || {}; - if (name in ICAL.design.param && "valueType" in ICAL.design.param[name]) { - paramType = ICAL.design.param[name].valueType; + // attempt to determine value + if (!('value' in params)) { + if (propertyDetails) { + valueType = propertyDetails.defaultType; + } else { + valueType = DEFAULT_TYPE; } - var paramData = { - value: aLineData.parameters[name], - type: paramType - }; + } else { + // possible to avoid this? + valueType = params.value.toLowerCase(); + delete params.value; + } - aLineData.parameters[name] = paramData; + /** + * 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); }; - parser.detectValueType = function detectValueType(aLineData) { - var valueType = "TEXT"; - var defaultType = null; - if (aLineData.name in ICAL.design.property && - "defaultType" in ICAL.design.property[aLineData.name]) { - valueType = ICAL.design.property[aLineData.name].defaultType; + /** + * @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 ("parameters" in aLineData && "VALUE" in aLineData.parameters) { - var valueParam = aLineData.parameters.VALUE; - if (typeof(valueParam) === 'string') { - valueType = aLineData.parameters.VALUE.toUpperCase(); - } else if(typeof(valueParam) === 'object') { - valueType = valueParam.value.toUpperCase(); + if (name in design.param && design.param[name].valueType) { + type = design.param[name].valueType; } + + result[parser._formatName(name)] = parser._parseValue(value, type); } - if (!(valueType in ICAL.design.value)) { - throw new ParserError(aLineData, "Invalid VALUE Type '" + valueType); + 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; } - aLineData.type = valueType; + // 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]; - // It could be a multi-value value, we have to take that apart first - function unwrapMultiValue(x, separator) { - var values = []; + if (firstChar === ' ' || firstChar === '\t') { + // add to line + line += buffer.substr( + lastPos + 1, + pos - lastPos - (newlineOffset + 1) + ); + } else { + if (line) + callback(null, line); + // push line + line = buffer.substr( + lastPos, + pos - lastPos - newlineOffset + ); + } + + lastPos = pos; + } while (pos !== len); + + // extra ending line + line = line.trim(); + + if (line.length) + callback(null, line); + } + + return parser; + +}()); +/* This Source Code Form is subject to the terms of the Mozilla Public + console.log() + * 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 = {} : ''; +(function() { + + /** + * home of the original parser, we have re-purposed most + * of the decoration parsing here in the icaltype classes (like period). + * Eventually we need to to through each of these and make sure + * they are as efficient as possible. Most of these are very complete + * and validate input but are slow due to their heavy use of RegExp. + */ + + function ParserError(aState, aMessage) { + this.mState = aState; + this.name = "ParserError"; + if (aState) { + var lineNrData = ("lineNr" in aState ? aState.lineNr + ":" : "") + + ("character" in aState && !isNaN(aState.character) ? + aState.character + ":" : + ""); - function replacer(s, a) { - values.push(a); - return ""; + var message = lineNrData + aMessage; + if ("buffer" in aState) { + if (aState.buffer) { + message += " before '" + aState.buffer + "'"; + } else { + message += " at end of line"; + } } - var re = new RegExp("(.*?[^\\\\])" + separator, "g"); - values.push(x.replace(re, replacer)); - return values; - } - - if (aLineData.name in ICAL.design.property) { - if (ICAL.design.property[aLineData.name].multiValue) { - aLineData.value = unwrapMultiValue(aLineData.value, ","); - } else if (ICAL.design.property[aLineData.name].structuredValue) { - aLineData.value = unwrapMultiValue(aLineData.value, ";"); - } else { - aLineData.value = [aLineData.value]; + if ("line" in aState) { + message += " in '" + aState.line + "'"; } + this.message = message; } else { - aLineData.value = [aLineData.value]; + this.message = aMessage; } - if ("unescape" in ICAL.design.value[valueType]) { - var unescaper = ICAL.design.value[valueType].unescape; - for (var idx in aLineData.value) { - aLineData.value[idx] = unescaper(aLineData.value[idx], aLineData.name); - } + // create stack + try { + throw new Error(); + } catch (e) { + var split = e.stack.split('\n'); + split.shift(); + this.stack = split.join('\n'); } - - return aLineData; } + ParserError.prototype = { + __proto__: Error.prototype, + constructor: ParserError + }; + + var parser = { + Error: ParserError + }; + + ICAL.DecorationParser = parser; + parser.validateValue = function validateValue(aLineData, aValueType, aValue, aCheckParams) { var propertyData = ICAL.design.property[aLineData.name]; @@ -590,57 +1553,6 @@ ICAL.helpers = { return parser.validateValue(lineData, aType, aStr, false); }; - parser.decorateValue = function decorateValue(aType, aValue) { - if (aType in ICAL.design.value && "decorate" in ICAL.design.value[aType]) { - return ICAL.design.value[aType].decorate(aValue); - } else { - return ICAL.design.value.TEXT.decorate(aValue); - } - }; - - parser.stringifyProperty = function stringifyProperty(aLineData) { - ICAL.helpers.dumpn("Stringify: " + aLineData.toString()); - var str = aLineData.name; - if (aLineData.parameters) { - for (var key in aLineData.parameters) { - str += ";" + key + "=" + aLineData.parameters[key].value; - } - } - - str += ":" + parser.stringifyValue(aLineData); - - return ICAL.helpers.foldline(str); - }; - - parser.stringifyValue = function stringifyValue(aLineData) { - function arrayStringMap(arr, func) { - var newArr = []; - for (var idx in arr) { - newArr[idx] = func(arr[idx].toString()); - } - return newArr; - } - - if (aLineData) { - var values = aLineData.value; - if (aLineData.type in ICAL.design.value && - "escape" in ICAL.design.value[aLineData.type]) { - var escaper = ICAL.design.value[aLineData.type].escape; - values = arrayStringMap(values, escaper); - } - - var separator = ","; - if (aLineData.name in ICAL.design.property && - ICAL.design.property[aLineData.name].structuredValue) { - separator = ";"; - } - - return values.join(separator); - } else { - return null; - } - }; - parser.parseDateOrDateTime = function parseDateOrDateTime(aState) { var data = parser.parseDate(aState); @@ -734,7 +1646,7 @@ ICAL.helpers = { function parseDurWeek(aState) { return { - weeks: parser.expectRE(aState, /^((\d+)W)/, "Expected Weeks")[2] + weeks: parseInt(parser.expectRE(aState, /^((\d+)W)/, "Expected Weeks")[2], 10) }; } @@ -758,7 +1670,7 @@ ICAL.helpers = { } if (data) { - data.days = days[2]; + data.days = parseInt(days[2], 10); } else { data = { days: parseInt(days[2], 10) @@ -1079,939 +1991,694 @@ ICAL.helpers = { throw new ParserError(aState, aErrorMessage); } } - - /* Possible shortening: - - pro: retains order - - con: datatypes not obvious - - pro: not so many objects created - - { - "begin:vcalendar": [ - { - prodid: "-//Example Inc.//Example Client//EN", - version: "2.0" - "begin:vtimezone": [ - { - "last-modified": [{ - type: "date-time", - value: "2004-01-10T03:28:45Z" - }], - tzid: "US/Eastern" - "begin:daylight": [ - { - dtstart: { - type: "date-time", - value: "2000-04-04T02:00:00" - } - rrule: { - type: "recur", - value: { - freq: "YEARLY", - byday: ["1SU"], - bymonth: ["4"], - } - } - } - ] - } - ], - "begin:vevent": [ - { - category: [{ - type: "text" - // have icalcomponent take apart the multivalues - value: "multi1,multi2,multi3" - },{ - type "text" - value: "otherprop1" - }] - } - ] - } - ] - } - */ })(); -/* 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 */ +ICAL.Component = (function() { + 'use strict'; -(typeof(ICAL) === 'undefined')? ICAL = {} : ''; + var PROPERTY_INDEX = 1; + var COMPONENT_INDEX = 2; + var NAME_INDEX = 0; -/** - * Design data used by the parser to decide if data is semantically correct - */ -ICAL.design = { - 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: true - }, - "DELEGATED-TO": { - valueType: "CAL-ADDRESS", - multiValue: 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: 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": { - valueType: "BOOLEAN" - }, - "SENT-BY": { - valueType: "CAL-ADDRESS" - }, - "TZID": { - matches: /^\// - }, - "VALUE": { - values: ["BINARY", "BOOLEAN", "CAL-ADDRESS", "DATE", "DATE-TIME", - "DURATION", "FLOAT", "INTEGER", "PERIOD", "RECUR", "TEXT", - "TIME", "URI", "UTC-OFFSET"], - allowXName: true, - allowIanaToken: true + /** + * 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, [], []]; } - }, - // When adding a value here, be sure to add it to the parameter types! - value: { + // mostly for legacy reasons. + this.jCal = jCal; - "BINARY": { - matches: /^([A-Za-z0-9+\/]{4})*([A-Za-z0-9+\/]{2}==|[A-Za-z0-9+\/]{3}=)?$/, - requireParam: { - "ENCODING": "BASE64" - }, - decorate: function(aString) { - return ICAL.icalbinary.fromString(aString); - } - }, - "BOOLEAN": { - values: ["TRUE", "FALSE"], - decorate: function(aValue) { - return ICAL.icalvalue.fromString(aValue, "BOOLEAN"); - } - }, - "CAL-ADDRESS": { - // needs to be an uri - }, - "DATE": { - validate: function(aValue) { - var state = { - buffer: aValue - }; - var data = ICAL.icalparser.parseDate(state); - ICAL.icalparser.expectEnd(state, "Junk at end of DATE value"); - return data; - }, - decorate: function(aValue) { - return ICAL.icaltime.fromString(aValue); - } - }, - "DATE-TIME": { - validate: function(aValue) { - var state = { - buffer: aValue - }; - var data = ICAL.icalparser.parseDateTime(state); - ICAL.icalparser.expectEnd(state, "Junk at end of DATE-TIME value"); - return data; - }, + if (parent) { + this.parent = parent; + } + } - decorate: function(aValue) { - return ICAL.icaltime.fromString(aValue); - } - }, - "DURATION": { - validate: function(aValue) { - var state = { - buffer: aValue - }; - var data = ICAL.icalparser.parseDuration(state); - ICAL.icalparser.expectEnd(state, "Junk at end of DURATION value"); - return data; - }, - decorate: function(aValue) { - return ICAL.icalduration.fromString(aValue); - } - }, - "FLOAT": { - matches: /^[+-]?\d+\.\d+$/, - decorate: function(aValue) { - return ICAL.icalvalue.fromString(aValue, "FLOAT"); - } - }, - "INTEGER": { - matches: /^[+-]?\d+$/, - decorate: function(aValue) { - return ICAL.icalvalue.fromString(aValue, "INTEGER"); - } - }, - "PERIOD": { - validate: function(aValue) { - var state = { - buffer: aValue - }; - var data = ICAL.icalparser.parsePeriod(state); - ICAL.icalparser.expectEnd(state, "Junk at end of PERIOD value"); - return data; - }, + Component.prototype = { - decorate: function(aValue) { - return ICAL.icalperiod.fromString(aValue); - } + get name() { + return this.jCal[NAME_INDEX]; }, - "RECUR": { - validate: function(aValue) { - var state = { - buffer: aValue - }; - var data = ICAL.icalparser.parseRecur(state); - ICAL.icalparser.expectEnd(state, "Junk at end of RECUR value"); - return data; - }, - decorate: function decorate(aValue) { - return ICAL.icalrecur.fromString(aValue); + _hydrateComponent: function(index) { + if (!this._components) { + this._components = []; } - }, - "TEXT": { - matches: /.*/, - decorate: function(aValue) { - return ICAL.icalvalue.fromString(aValue, "TEXT"); - }, - unescape: function(aValue, aName) { - return aValue.replace(/\\\\|\\;|\\,|\\[Nn]/g, function(str) { - switch (str) { - case "\\\\": - return "\\"; - case "\\;": - return ";"; - case "\\,": - return ","; - case "\\n": - case "\\N": - return "\n"; - default: - return str; - } - }); - }, - - escape: 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; - } - }); + if (this._components[index]) { + return this._components[index]; } - }, - "TIME": { - validate: function(aValue) { - var state = { - buffer: aValue - }; - var data = ICAL.icalparser.parseTime(state); - ICAL.icalparser.expectEnd(state, "Junk at end of TIME value"); - return data; - } - }, + var comp = new Component( + this.jCal[COMPONENT_INDEX][index], + this + ); - "URI": { - // TODO - /* ... */ + return this._components[index] = comp; }, - "UTC-OFFSET": { - validate: function(aValue) { - var state = { - buffer: aValue - }; - var data = ICAL.icalparser.parseUtcOffset(state); - ICAL.icalparser.expectEnd(state, "Junk at end of UTC-OFFSET value"); - return data; - }, + _hydrateProperty: function(index) { + if (!this._properties) { + this._properties = []; + } - decorate: function(aValue) { - return ICAL.icalutcoffset.fromString(aValue); + if (this._properties[index]) { + return this._properties[index]; } - } - }, - property: { - decorate: function decorate(aData, aParent) { - return new ICAL.icalproperty(aData, aParent); - }, - "ATTACH": { - defaultType: "URI" - }, - "ATTENDEE": { - defaultType: "CAL-ADDRESS" - }, - "CATEGORIES": { - defaultType: "TEXT", - multiValue: true - }, - "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"] - }, - "EXRULE": { - defaultType: "RECUR" - }, - "FREEBUSY": { - defaultType: "PERIOD", - multiValue: true - }, - "GEO": { - defaultType: "FLOAT", - structuredValue: true - }, - /* 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"] - }, - "RECURRENCE-ID": { - defaultType: "DATE-TIME", - allowedTypes: ["DATE-TIME", "DATE"] - }, - "RESOURCES": { - defaultType: "TEXT", - multiValue: true - }, - "REQUEST-STATUS": { - defaultType: "TEXT", - structuredValue: true - }, - "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" - } - }, + var prop = new ICAL.Property( + this.jCal[PROPERTY_INDEX][index], + this + ); - component: { - decorate: function decorate(aData, aParent) { - return new ICAL.icalcomponent(aData, aParent); + return this._properties[index] = prop; }, - "VEVENT": {} - } -}; -/* 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 */ + /** + * 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); + } + } -(typeof(ICAL) === 'undefined')? ICAL = {} : ''; -(function() { - ICAL.icalcomponent = function icalcomponent(data, parent) { - this.wrappedJSObject = this; - this.parent = parent; - this.fromData(data); - } + // ensure we return a value (strict mode) + return null; + }, - ICAL.icalcomponent.prototype = { + /** + * 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; - data: null, - name: "", - components: null, - properties: null, + if (name) { + var comps = this.jCal[COMPONENT_INDEX]; + var result = []; + var i = 0; - icalclass: "icalcomponent", + for (; i < jCalLen; i++) { + if (name === comps[i][NAME_INDEX]) { + result.push( + this._hydrateComponent(i) + ); + } + } + return result; + } else { + if (!this._components || + (this._components.length !== jCalLen)) { + var i = 0; + for (; i < jCalLen; i++) { + this._hydrateComponent(i); + } + } - clone: function clone() { - return new ICAL.icalcomponent(this.undecorate(), this.parent); + return this._components; + } }, - fromData: function fromData(data) { - if (!data) { - data = ICAL.helpers.initComponentData(null); - } - this.data = data; - this.data.value = this.data.value || []; - this.data.type = this.data.type || "COMPONENT"; - this.components = {}; - this.properties = {}; - - // Save the name directly on the object, as we want this accessed - // from the outside. - this.name = this.data.name; - delete this.data.name; - - var value = this.data.value; - - for (var key in value) { - var keyname = value[key].name; - if (value[key].type == "COMPONENT") { - value[key] = new ICAL.icalcomponent(value[key], this); - ICAL.helpers.ensureKeyExists(this.components, keyname, []); - this.components[keyname].push(value[key]); - } else { - value[key] = new ICAL.icalproperty(value[key], this); - ICAL.helpers.ensureKeyExists(this.properties, keyname, []); - this.properties[keyname].push(value[key]); + /** + * 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; } } - }, - undecorate: function undecorate() { - var newdata = []; - for (var key in this.data.value) { - newdata.push(this.data.value[key].undecorate()); - } - return { - name: this.name, - type: "COMPONENT", - value: newdata - }; + return false; }, - getFirstSubcomponent: function getFirstSubcomponent(aType) { - var comp = null; - if (aType) { - var ucType = aType.toUpperCase(); - if (ucType in this.components && - this.components[ucType] && - this.components[ucType].length > 0) { - comp = this.components[ucType][0]; + /** + * 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 { - for (var thiscomp in this.components) { - comp = this.components[thiscomp][0]; - break; + if (this.jCal[PROPERTY_INDEX].length) { + return this._hydrateProperty(0); } } - return comp; + + 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; }, - getAllSubcomponents: function getAllSubcomponents(aType) { - var comps = []; - if (aType && aType != "ANY") { - var ucType = aType.toUpperCase(); - if (ucType in this.components) { - for (var compKey in this.components[ucType]) { - comps.push(this.components[ucType][compKey]); + /** + * 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 { - for (var compName in this.components) { - for (var compKey in this.components[compName]) { - comps.push(this.components[compName][compKey]); + if (!this._properties || + (this._properties.length !== jCalLen)) { + var i = 0; + for (; i < jCalLen; i++) { + this._hydrateProperty(i); } } - } - return comps; - }, - addSubcomponent: function addSubcomponent(aComp, aCompName) { - var ucName, comp; - var comp; - if (aComp.icalclass == "icalcomponent") { - ucName = aComp.name; - comp = aComp.clone(); - comp.parent = this; - } else { - ucName = aCompName.toUpperCase(); - comp = new ICAL.icalcomponent(aComp, ucName, this); + return this._properties; } - this.data.value.push(comp); - ICAL.helpers.ensureKeyExists(this.components, ucName, []); - this.components[ucName].push(comp); + return null; }, - removeSubcomponent: function removeSubComponent(aName) { - var ucName = aName.toUpperCase(); - for (var key in this.components[ucName]) { - var pos = this.data.value.indexOf(this.components[ucName][key]); - if (pos > -1) { - this.data.value.splice(pos, 1); - } + _removeObjectByIndex: function(jCalIndex, cache, index) { + // remove cached version + if (cache && cache[index]) { + cache.splice(index, 1); } - delete this.components[ucName]; + // remove it from the jCal + this.jCal[jCalIndex].splice(index, 1); }, - hasProperty: function hasProperty(aName) { - var ucName = aName.toUpperCase(); - return (ucName in this.properties); - }, + _removeObject: function(jCalIndex, cache, nameOrObject) { + var i = 0; + var objects = this.jCal[jCalIndex]; + var len = objects.length; + var cached = this[cache]; - getFirstProperty: function getFirstProperty(aName) { - var prop = null; - if (aName) { - var ucName = aName.toUpperCase(); - if (ucName in this.properties && this.properties[ucName]) { - prop = this.properties[ucName][0]; + if (typeof(nameOrObject) === 'string') { + for (; i < len; i++) { + if (objects[i][NAME_INDEX] === nameOrObject) { + this._removeObjectByIndex(jCalIndex, cached, i); + return true; + } } - } else { - for (var p in this.properties) { - prop = this.properties[p]; - break; + } else if (cached) { + for (; i < len; i++) { + if (cached[i] && cached[i] === nameOrObject) { + this._removeObjectByIndex(jCalIndex, cached, i); + return true; + } } } - return prop; + + return false; }, - getFirstPropertyValue: function getFirstPropertyValue(aName) { - // TODO string value? - var prop = this.getFirstProperty(aName); - return (prop ? prop.getFirstValue() : null); - }, + _removeAllObjects: function(jCalIndex, cache, name) { + var cached = this[cache]; + + if (name) { + var objects = this.jCal[jCalIndex]; + var i = objects.length - 1; - getAllProperties: function getAllProperties(aName) { - var props = []; - if (aName && aName != "ANY") { - var ucType = aName.toUpperCase(); - if (ucType in this.properties) { - props = this.properties[ucType].concat([]); + // 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 { - for (var propName in this.properties) { - props = props.concat(this.properties[propName]); + 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; } - return props; }, /** - * Adds or replaces a property with a given value. - * Suitable for use when updating properties which - * are expected to only have a single value (like DTSTART, SUMMARY, etc..) + * Adds a single sub component. * - * @param {String} aName property name. - * @param {Object} aValue property value. + * @param {ICAL.Component} component to add. */ - updatePropertyWithValue: function updatePropertyWithValue(aName, aValue) { - if (!this.hasProperty(aName)) { - return this.addPropertyWithValue(aName, aValue); + addSubcomponent: function(component) { + if (!this._components) { + this._components = []; } - var prop = this.getFirstProperty(aName); - - var lineData = ICAL.icalparser.detectValueType({ - name: aName.toUpperCase(), - value: aValue - }); - - prop.setValues(lineData.value, lineData.type); - - return prop; + var idx = this.jCal[COMPONENT_INDEX].push(component.jCal); + this._components[idx - 1] = component; }, - addPropertyWithValue: function addStringProperty(aName, aValue) { - var ucName = aName.toUpperCase(); - var lineData = ICAL.icalparser.detectValueType({ - name: ucName, - value: aValue - }); + /** + * 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) { + return this._removeObject(COMPONENT_INDEX, '_components', nameOrComp); + }, - var prop = ICAL.icalproperty.fromData(lineData); - ICAL.helpers.dumpn("Adding property " + ucName + "=" + aValue); - return this.addProperty(prop); + /** + * Removes all components or (if given) all + * components by a particular name. + * + * @param {String} [name] (lowercase) component name. + */ + removeAllSubcomponents: function(name) { + return this._removeAllObjects(COMPONENT_INDEX, '_components', name); }, - addProperty: function addProperty(aProp) { - var prop = aProp; - if (aProp.parent) { - prop = aProp.clone(); + /** + * 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'); } - aProp.parent = this; - ICAL.helpers.ensureKeyExists(this.properties, aProp.name, []); - this.properties[aProp.name].push(aProp); - ICAL.helpers.dumpn("DATA IS: " + this.data.toString()); - this.data.value.push(aProp); - ICAL.helpers.dumpn("Adding property " + aProp); - }, + var idx = this.jCal[PROPERTY_INDEX].push(property.jCal); + property.component = this; - removeProperty: function removeProperty(aName) { - var ucName = aName.toUpperCase(); - for (var key in this.properties[ucName]) { - var pos = this.data.value.indexOf(this.properties[ucName][key]); - if (pos > -1) { - this.data.value.splice(pos, 1); - } + if (!this._properties) { + this._properties = []; } - delete this.properties[ucName]; - }, - clearAllProperties: function clearAllProperties() { - this.properties = {}; - for (var i = this.data.value.length - 1; i >= 0; i--) { - if (this.data.value[i].type != "COMPONENT") { - delete this.data.value[i]; - } - } + this._properties[idx - 1] = property; }, - _valueToJSON: function(value) { - if (value && value.icaltype) { - return value.toString(); - } + /** + * 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); - if (typeof(value) === 'object') { - return this._undecorateJSON(value); - } + this.addProperty(prop, this); - return value; + return prop; }, - _undecorateJSON: function(object) { - if (object instanceof Array) { - var result = []; - var len = object.length; - - for (var i = 0; i < len; i++) { - result.push(this._valueToJSON(object[i])); - } + /** + * Helper method that will update or create a property + * of the given name and sets its value. + * + * @param {String} name property name. + * @param {Object} value property value. + * @return {ICAL.Property} property. + */ + updatePropertyWithValue: function(name, value) { + var prop = this.getFirstProperty(name); + if (prop) { + prop.setValue(value); } else { - var result = {}; - var key; - - for (key in object) { - if (object.hasOwnProperty(key)) { - result[key] = this._valueToJSON(object[key]); - } - } + prop = this.addPropertyWithValue(name, value); } - return result; + return prop; }, /** - * Exports the components values to a json friendly - * object. You can use JSON.stringify directly on - * components as a result. + * Removes a single property by name or + * the instance of the specific property. + * + * @param {String|ICAL.Property} nameOrProp to remove. + * @return {Boolean} true when deleted. */ - toJSON: function toJSON() { - return this._undecorateJSON(this.undecorate()); + removeProperty: function(nameOrProp) { + return this._removeObject(PROPERTY_INDEX, '_properties', nameOrProp); }, - toString: function toString() { - var str = ICAL.helpers.foldline("BEGIN:" + this.name) + ICAL.newLineChar; - for (var key in this.data.value) { - str += this.data.value[key].toString() + ICAL.newLineChar; - } - str += ICAL.helpers.foldline("END:" + this.name); - return str; + /** + * Removes all properties associated with this component. + * + * @param {String} [name] (lowecase) optional property name. + */ + removeAllProperties: function(name) { + return this._removeAllObjects(PROPERTY_INDEX, '_properties', name); + }, + + toJSON: function() { + return this.jCal; + }, + + toString: function() { + return ICAL.stringify.component( + this.jCal + ); } - }; - ICAL.icalcomponent.fromString = function icalcomponent_from_string(str) { - return ICAL.icalcomponent.fromData(ICAL.parse(str)); }; - ICAL.icalcomponent.fromData = function icalcomponent_from_data(aData) { - return new ICAL.icalcomponent(aData); - }; -})(); -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - * Portions Copyright (C) Philipp Kewisch, 2011-2012 */ + return Component; +}()); +ICAL.Property = (function() { + 'use strict'; + var NAME_INDEX = 0; + var PROP_INDEX = 1; + var TYPE_INDEX = 2; + var VALUE_INDEX = 3; -(typeof(ICAL) === 'undefined')? ICAL = {} : ''; -(function() { - ICAL.icalproperty = function icalproperty(data, parent) { - this.wrappedJSObject = this; - this.parent = parent; - this.fromData(data); - } + var design = ICAL.design; - ICAL.icalproperty.prototype = { - parent: null, - data: null, - name: null, - icalclass: "icalproperty", + /** + * Provides a nicer interface to any kind of property. + * Its important to note that mutations done in the wrapper + * directly effect (mutate) the jCal object used to initialize. + * + * Can also be used to create new properties by passing + * the name of the property (as a String). + * + * + * @param {Array|String} jCal raw jCal representation OR + * the new name of the property (when creating). + * + * @param {ICAL.Component} [component] parent component. + */ + function Property(jCal, component) { + if (typeof(jCal) === 'string') { + // because we a creating by name we need + // to find the type when creating the property. + var name = jCal; + + if (name in design.property) { + var prop = design.property[name]; + if ('defaultType' in prop) { + var type = prop.defaultType; + } else { + var type = design.defaultType; + } + } else { + var type = design.defaultType; + } - clone: function clone() { - return new ICAL.icalproperty(this.undecorate(), this.parent); + jCal = [name, {}, type]; + } + + this.jCal = jCal; + this.component = component; + this._updateType(); + } + + Property.prototype = { + get type() { + return this.jCal[TYPE_INDEX]; }, - fromData: function fromData(aData) { - if (!aData.name) { - ICAL.helpers.dumpn("Missing name: " + aData.toString()); - } - this.name = aData.name; - this.data = aData; - this.setValues(this.data.value, this.data.type); - delete this.data.name; + get name() { + return this.jCal[NAME_INDEX]; }, - undecorate: function() { - var values = []; - for (var key in this.data.value) { - var val = this.data.value[key]; - if ("undecorate" in val) { - values.push(val.undecorate()); + _updateType: function() { + if (this.type in design.value) { + var designType = design.value[this.type]; + + if ('decorate' in design.value[this.type]) { + this.isDecorated = true; } else { - values.push(val); + this.isDecorated = false; + } + + if (this.name in design.property) { + if ('multiValue' in design.property[this.name]) { + this.isMultiValue = true; + } else { + this.isMultiValue = false; + } } } - var obj = { - name: this.name, - type: this.data.type, - value: values - }; - if (this.data.parameters) { - obj.parameters = this.data.parameters; + }, + + /** + * Hydrate a single value. + */ + _hydrateValue: function(index) { + if (this._values && this._values[index]) { + return this._values[index]; + } + + // for the case where there is no value. + if (this.jCal.length <= (VALUE_INDEX + index)) { + return null; + } + + if (this.isDecorated) { + if (!this._values) { + this._values = []; + } + return this._values[index] = this._decorate( + this.jCal[VALUE_INDEX + index] + ); + } else { + return this.jCal[VALUE_INDEX + index]; } - return obj; }, - toString: function toString() { - return ICAL.icalparser.stringifyProperty({ - name: this.name, - type: this.data.type, - value: this.data.value, - parameters: this.data.parameters - }); + _decorate: function(value) { + return design.value[this.type].decorate(value); }, - getStringValue: function getStringValue() { - ICAL.helpers.dumpn("GV: " + ICAL.icalparser.stringifyValue(this.data)); - return ICAL.icalparser.stringifyValue(this.data); + _undecorate: function(value) { + return design.value[this.type].undecorate(value); }, - setStringValue: function setStringValue(val) { - this.setValue(val, this.data.type); - // TODO force TEXT or rename method to something like setParseValue() + _setDecoratedValue: function(value, index) { + if (!this._values) { + this._values = []; + } + + if (typeof(value) === 'object' && 'icaltype' in value) { + // decorated value + this.jCal[VALUE_INDEX + index] = this._undecorate(value); + this._values[index] = value; + } else { + // undecorated value + this.jCal[VALUE_INDEX + index] = value; + this._values[index] = this._decorate(value); + } }, - getFirstValue: function getValue() { - return (this.data.value ? this.data.value[0] : null); + /** + * Gets a param on the property. + * + * @param {String} name prop name (lowercase). + * @return {String} prop value. + */ + getParameter: function(name) { + return this.jCal[PROP_INDEX][name]; }, - getValues: function getValues() { - return (this.data.value ? this.data.value : []); + /** + * Sets a param on the property. + * + * @param {String} value property value. + */ + setParameter: function(name, value) { + this.jCal[PROP_INDEX][name] = value; }, - setValue: function setValue(aValue, aType) { - return this.setValues([aValue], aType); + /** + * Removes a parameter + * + * @param {String} name prop name (lowercase). + */ + removeParameter: function(name) { + return delete this.jCal[PROP_INDEX][name]; }, - setValues: function setValues(aValues, aType) { - var newValues = []; - var newType = null; - for (var key in aValues) { - var value = aValues[key]; - if (value.icalclass && value.icaltype) { - if (newType && newType != value.icaltype) { - throw new Error("All values must be of the same type!"); - } else { - newType = value.icaltype; - } - newValues.push(value); - } else { - var type; - if (aType) { - type = aType; - } else if (typeof value == "string") { - type = "TEXT"; - } else if (typeof value == "number") { - type = (Math.floor(value) == value ? "INTEGER" : "FLOAT"); - } else if (typeof value == "boolean") { - type = "BOOLEAN"; - value = (value ? "TRUE" : "FALSE"); - } else { - throw new ParserError(null, "Invalid value: " + value); - } + /** + * Sets type of property and clears out any + * existing values of the current type. + * + * @param {String} type new iCAL type (see design.values). + */ + resetType: function(type) { + this.removeAllValues(); + this.jCal[TYPE_INDEX] = type; + this._updateType(); + }, - if (newType && newType != type) { - throw new Error("All values must be of the same type!"); - } else { - newType = type; - } - ICAL.icalparser.validateValue(this.data, type, "" + value, true); - newValues.push(ICAL.icalparser.decorateValue(type, "" + value)); - } + /** + * Finds first property value. + * + * @return {String} first property value. + */ + getFirstValue: function() { + return this._hydrateValue(0); + }, + + /** + * Gets all values on the property. + * + * NOTE: this creates an array during each call. + * + * @return {Array} list of values. + */ + getValues: function() { + var len = this.jCal.length - VALUE_INDEX; + + if (len < 1) { + // its possible for a property to have no value. + return; } - this.data.value = newValues; - this.data.type = newType; - return aValues; - }, + var i = 0; + var result = []; - getValueType: function getValueType() { - return this.data.type; - }, + for (; i < len; i++) { + result[i] = this._hydrateValue(i); + } - getName: function getName() { - return this.name; + return result; }, - getParameterValue: function getParameter(aName) { - var value = null; - var ucName = aName.toUpperCase(); - if (ICAL.helpers.hasKey(this.data.parameters, ucName)) { - value = this.data.parameters[ucName].value; + removeAllValues: function() { + if (this._values) { + this._values.length = 0; } - return value; + this.jCal.length = 3; }, - getParameterType: function getParameterType(aName) { - var type = null; - var ucName = aName.toUpperCase(); - if (ICAL.helpers.hasKey(this.data.parameters, ucName)) { - type = this.data.parameters[ucName].type; + /** + * Sets the values of the property. + * Will overwrite the existing values. + * + * @param {Array} values an array of values. + */ + setValues: function(values) { + if (!this.isMultiValue) { + throw new Error( + this.name + ': does not not support mulitValue.\n' + + 'override isMultiValue' + ); } - return type; - }, - setParameter: function setParameter(aName, aValue, aType) { - // TODO autodetect type by name - var ucName = aName.toUpperCase(); - ICAL.helpers.ensureKeyExists(this.data, "parameters", {}); - this.data.parameters[ucName] = { - type: aType || "TEXT", - value: aValue - }; + var len = values.length; + var i = 0; + this.removeAllValues(); + + if (this.isDecorated) { + for (; i < len; i++) { + this._setDecoratedValue(values[i], i); + } + } else { + for (; i < len; i++) { + this.jCal[VALUE_INDEX + i] = values[i]; + } + } + }, - if (aName == "VALUE") { - this.data.type = aValue; - // TODO revalidate value + /** + * Sets the current value of the property. + * + * @param {String|Object} value new prop value. + */ + setValue: function(value) { + if (this.isDecorated) { + this._setDecoratedValue(value, 0); + } else { + this.jCal[VALUE_INDEX] = value; } }, - countParameters: function countParmeters() { - // TODO Object.keys compatibility? - var dp = this.data.parameters; - return (dp ? Object.keys(dp).length : 0); + /** + * Returns the jCal representation of this property. + * + * @return {Object} jCal. + */ + toJSON: function() { + return this.jCal; }, - removeParameter: function removeParameter(aName) { - var ucName = aName.toUpperCase(); - if (ICAL.helpers.hasKey(this.data.parameters, ucName)) { - delete this.data.parameters[ucName]; - } + toICAL: function() { + return ICAL.stringify.property( + this.jCal + ); } - }; - ICAL.icalproperty.fromData = function(aData) { - return new ICAL.icalproperty(aData); }; -})(); + + return Property; + +}()); /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. @@ -2045,7 +2712,7 @@ ICAL.design = { fromString: function icalvalue_fromString(aString, aType) { var type = aType || this.icaltype; - this.fromData(ICAL.icalparser.parseValue(aString, type), type); + this.fromData(ICAL.DecorationParser.parseValue(aString, type), type); }, undecorate: function icalvalue_undecorate() { @@ -2072,14 +2739,14 @@ ICAL.design = { }; ICAL.icalbinary = function icalbinary(aData, aParent) { - ICAL.icalvalue.call(this, aData, aParent, "BINARY"); + ICAL.icalvalue.call(this, aData, aParent, "binary"); }; ICAL.icalbinary.prototype = { __proto__: ICAL.icalvalue.prototype, - icaltype: "BINARY", + icaltype: "binary", decodeValue: function decodeValue() { return this._b64_decode(this.data.value); @@ -2200,7 +2867,7 @@ ICAL.design = { ICAL.icalvalue._createFromString(ICAL.icalbinary); ICAL.icalutcoffset = function icalutcoffset(aData, aParent) { - ICAL.icalvalue.call(this, aData, aParent, "UTC-OFFSET"); + ICAL.icalvalue.call(this, aData, aParent, "utc-offset"); }; ICAL.icalutcoffset.prototype = { @@ -2211,7 +2878,7 @@ ICAL.design = { minutes: null, factor: null, - icaltype: "UTC-OFFSET", + icaltype: "utc-offset", fromData: function fromData(aData) { if (aData) { @@ -2249,7 +2916,7 @@ ICAL.design = { end: null, duration: null, icalclass: "icalperiod", - icaltype: "PERIOD", + icaltype: "period", getDuration: function duration() { if (this.duration) { @@ -2273,9 +2940,10 @@ ICAL.design = { }; ICAL.icalperiod.fromString = function fromString(str) { - var data = ICAL.icalparser.parseValue(str, "PERIOD"); + var data = ICAL.DecorationParser.parseValue(str, "period"); return ICAL.icalperiod.fromData(data); }; + ICAL.icalperiod.fromData = function fromData(aData) { return new ICAL.icalperiod(aData); }; @@ -2289,6 +2957,8 @@ ICAL.design = { (typeof(ICAL) === 'undefined')? ICAL = {} : ''; (function() { + var DURATION_LETTERS = /([PDWHMTS]{1,1})/; + ICAL.icalduration = function icalduration(data) { this.wrappedJSObject = this; this.fromData(data); @@ -2303,7 +2973,7 @@ ICAL.design = { seconds: 0, isNegative: false, icalclass: "icalduration", - icaltype: "DURATION", + icaltype: "duration", clone: function clone() { return ICAL.icalduration.fromData(this); @@ -2403,9 +3073,59 @@ ICAL.design = { return (new ICAL.icalduration()).fromSeconds(); }; + /** + * Internal helper function to handle a chunk of a duration. + * + * @param {String} letter type of duration chunk. + * @param {String} number numeric value or -/+. + * @param {Object} dict target to assign values to. + */ + function parseDurationChunk(letter, number, object) { + var type; + switch (letter) { + case 'P': + if (number && number === '-') { + object.isNegative = true; + } else { + object.isNegative = false; + } + // period + break; + case 'D': + type = 'days'; + break; + case 'W': + type = 'weeks'; + break; + case 'H': + type = 'hours'; + break; + case 'M': + type = 'minutes'; + break; + case 'S': + type = 'seconds'; + break; + } + + if (type) { + object[type] = parseInt(number); + } + } + ICAL.icalduration.fromString = function icalduration_from_string(aStr) { - var data = ICAL.icalparser.parseValue(aStr, "DURATION"); - return ICAL.icalduration.fromData(data); + var pos = 0; + var dict = Object.create(null); + + while ((pos = aStr.search(DURATION_LETTERS)) !== -1) { + var type = aStr[pos]; + var numeric = aStr.substr(0, pos); + aStr = aStr.substr(pos + 1); + + parseDurationChunk(type, numeric, dict); + } + + return new ICAL.icalduration(dict); }; ICAL.icalduration.fromData = function icalduration_from_data(aData) { @@ -2808,7 +3528,7 @@ ICAL.design = { auto_normalize: false, icalclass: "icaltime", - icaltype: "DATE-TIME", + icaltype: "date-time", clone: function icaltime_clone() { return new ICAL.icaltime(this); @@ -2835,10 +3555,10 @@ ICAL.design = { fromString: function icaltime_fromString(str) { var data; try { - data = ICAL.icalparser.parseValue(str, "DATE"); + data = ICAL.DecorationParser.parseValue(str, "date"); data.isDate = true; } catch (e) { - data = ICAL.icalparser.parseValue(str, "DATE-TIME"); + data = ICAL.DecorationParser.parseValue(str, "date-time"); data.isDate = false; } return this.fromData(data); @@ -3338,7 +4058,7 @@ ICAL.design = { this.minute = 0; this.second = 0; } - this.icaltype = (this.isDate ? "DATE" : "DATE-TIME"); + this.icaltype = (this.isDate ? "date" : "date-time"); this.adjust(0, 0, 0, 0); return this; @@ -3571,6 +4291,15 @@ ICAL.design = { return tt; }; + ICAL.icaltime.fromStringv2 = function fromString(str) { + return new ICAL.icaltime({ + year: parseInt(str.substr(0, 4), 10), + month: parseInt(str.substr(5, 2), 10), + day: parseInt(str.substr(8, 2), 10), + isDate: true + }); + }; + ICAL.icaltime.fromString = function fromString(str) { var tt = new ICAL.icaltime(); return tt.fromString(str); @@ -3625,6 +4354,7 @@ ICAL.design = { [0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366] ]; + ICAL.icaltime.SUNDAY = 1; ICAL.icaltime.MONDAY = 2; ICAL.icaltime.TUESDAY = 3; @@ -3632,6 +4362,8 @@ ICAL.design = { ICAL.icaltime.THURSDAY = 5; ICAL.icaltime.FRIDAY = 6; ICAL.icaltime.SATURDAY = 7; + + ICAL.icaltime.DEFAULT_WEEK_START = ICAL.icaltime.MONDAY; })(); /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this @@ -3644,15 +4376,21 @@ ICAL.design = { (function() { var DOW_MAP = { - SU: 1, - MO: 2, - TU: 3, - WE: 4, - TH: 5, - FR: 6, - SA: 7 + SU: ICAL.icaltime.SUNDAY, + MO: ICAL.icaltime.MONDAY, + TU: ICAL.icaltime.TUESDAY, + WE: ICAL.icaltime.WEDNESDAY, + TH: ICAL.icaltime.THURSDAY, + FR: ICAL.icaltime.FRIDAY, + SA: ICAL.icaltime.SATURDAY }; + var REVERSE_DOW_MAP = {}; + + for (var key in DOW_MAP) { + REVERSE_DOW_MAP[DOW_MAP[key]] = key; + } + ICAL.icalrecur = function icalrecur(data) { this.wrappedJSObject = this; this.parts = {}; @@ -3669,7 +4407,7 @@ ICAL.design = { count: null, freq: null, icalclass: "icalrecur", - icaltype: "RECUR", + icaltype: "recur", iterator: function(aStart) { return new ICAL.icalrecur_iterator({ @@ -3823,6 +4561,12 @@ ICAL.design = { for (var k in this.parts) { str += ";" + k + "=" + this.parts[k]; } + if (this.until ){ + str += ';UNTIL=' + this.until.toString(); + } + if ('wkst' in this && this.wkst !== ICAL.icaltime.DEFAULT_WEEK_START) { + str += ';WKST=' + REVERSE_DOW_MAP[this.wkst]; + } return str; }, @@ -3834,7 +4578,7 @@ ICAL.design = { value: [this.toString()] // TODO more props? }; - return ICAL.icalproperty.fromData(valueData); + return ICAL.Property.fromData(valueData); } catch (e) { ICAL.helpers.dumpn("EICALPROP: " + this.toString() + "//" + e); ICAL.helpers.dumpn(e.stack); @@ -3863,7 +4607,7 @@ ICAL.design = { } ICAL.icalrecur.fromString = function icalrecur_fromString(str) { - var data = ICAL.icalparser.parseValue(str, "RECUR"); + var data = ICAL.DecorationParser.parseValue(str, "recur"); return ICAL.icalrecur.fromData(data); }; @@ -5114,18 +5858,14 @@ ICAL.RecurExpansion = (function() { } function isRecurringComponent(comp) { - return comp.hasProperty('RDATE') || - comp.hasProperty('RRULE') || - comp.hasProperty('RECURRENCE-ID'); - } - - function propertyValue(prop) { - return prop.data.value[0]; + return comp.hasProperty('rdate') || + comp.hasProperty('rrule') || + comp.hasProperty('recurrence-id'); } /** * Primary class for expanding recurring rules. - * Can take multiple RRULEs, RDATEs, EXDATE(s) + * Can take multiple rrules, rdates, exdate(s) * and iterate (in order) over each next occurrence. * * Once initialized this class can also be serialized @@ -5135,8 +5875,8 @@ ICAL.RecurExpansion = (function() { * with ICAL.Event which handles recurrence exceptions. * * Options: - * - startDate: (ICAL.icaltime) start time of event (required) - * - component: (ICAL.icalcomponent) component (required unless resuming) + * - dtstart: (ICAL.icaltime) start time of event (required) + * - component: (ICAL.Component) component (required unless resuming) * * Examples: * @@ -5187,7 +5927,7 @@ ICAL.RecurExpansion = (function() { complete: false, /** - * Array of RRULE iterators. + * Array of rrule iterators. * * @type Array[ICAL.icalrecur_iterator] * @private @@ -5195,7 +5935,7 @@ ICAL.RecurExpansion = (function() { ruleIterators: null, /** - * Array of RDATE instances. + * Array of rdate instances. * * @type Array[ICAL.icaltime] * @private @@ -5203,7 +5943,7 @@ ICAL.RecurExpansion = (function() { ruleDates: null, /** - * Array of EXDATE instances. + * Array of exdate instances. * * @type Array[ICAL.icaltime] * @private @@ -5352,7 +6092,7 @@ ICAL.RecurExpansion = (function() { } //XXX: The spec states that after we resolve the final - // list of dates we execute EXDATE this seems somewhat counter + // list of dates we execute exdate this seems somewhat counter // intuitive to what I have seen most servers do so for now // I exclude based on the original date not the one that may // have been modified by the exception. @@ -5399,7 +6139,7 @@ ICAL.RecurExpansion = (function() { var idx; for (; i < len; i++) { - prop = propertyValue(props[i]); + prop = props[i].getFirstValue(); idx = ICAL.helpers.binsearchInsert( result, @@ -5419,8 +6159,17 @@ ICAL.RecurExpansion = (function() { this.last = this.dtstart.clone(); - if (component.hasProperty('RRULE')) { - var rules = component.getAllProperties('RRULE'); + // to provide api consistency non-recurring + // events can also use the iterator though it will + // only return a single time. + if (!isRecurringComponent(component)) { + this.ruleDate = this.last.clone(); + this.complete = true; + return; + } + + if (component.hasProperty('rrule')) { + var rules = component.getAllProperties('rrule'); var i = 0; var len = rules.length; @@ -5428,8 +6177,7 @@ ICAL.RecurExpansion = (function() { var iter; for (; i < len; i++) { - rule = propertyValue(rules[i]); - rule = new ICAL.icalrecur(rule); + rule = rules[i].getFirstValue(); iter = rule.iterator(this.dtstart); this.ruleIterators.push(iter); @@ -5440,8 +6188,8 @@ ICAL.RecurExpansion = (function() { } } - if (component.hasProperty('RDATE')) { - this.ruleDates = this._extractDates(component, 'RDATE'); + if (component.hasProperty('rdate')) { + this.ruleDates = this._extractDates(component, 'rdate'); this.ruleDateInc = ICAL.helpers.binsearchInsert( this.ruleDates, this.last, @@ -5451,8 +6199,8 @@ ICAL.RecurExpansion = (function() { this.ruleDate = this.ruleDates[this.ruleDateInc]; } - if (component.hasProperty('EXDATE')) { - this.exDates = this._extractDates(component, 'EXDATE'); + if (component.hasProperty('exdate')) { + this.exDates = this._extractDates(component, 'exdate'); // if we have a .last day we increment the index to beyond it. this.exDateInc = ICAL.helpers.binsearchInsert( this.exDates, @@ -5528,17 +6276,15 @@ ICAL.RecurExpansion = (function() { ICAL.Event = (function() { function Event(component, options) { - if (!(component instanceof ICAL.icalcomponent)) { + if (!(component instanceof ICAL.Component)) { options = component; component = null; } - if (!component) { - this.component = new ICAL.icalcomponent({ - name: 'VEVENT' - }); - } else { + if (component) { this.component = component; + } else { + this.component = new ICAL.Component('vevent'); } this.exceptions = Object.create(null); @@ -5566,14 +6312,14 @@ ICAL.Event = (function() { * If this component is an exception it cannot have other * exceptions related to it. * - * @param {ICAL.icalcomponent|ICAL.Event} obj component or event. + * @param {ICAL.Component|ICAL.Event} obj component or event. */ relateException: function(obj) { if (this.isRecurrenceException()) { throw new Error('cannot relate exception to exceptions'); } - if (obj instanceof ICAL.icalcomponent) { + if (obj instanceof ICAL.Component) { obj = new ICAL.Event(obj); } @@ -5635,11 +6381,11 @@ ICAL.Event = (function() { isRecurring: function() { var comp = this.component; - return comp.hasProperty('RRULE') || comp.hasProperty('RDATE'); + return comp.hasProperty('rrule') || comp.hasProperty('rdate'); }, isRecurrenceException: function() { - return this.component.hasProperty('RECURRENCE-ID'); + return this.component.hasProperty('recurrence-id'); }, /** @@ -5657,40 +6403,41 @@ ICAL.Event = (function() { * @return {Object} object of recurrence flags. */ getRecurrenceTypes: function() { - var rules = this.component.getAllProperties('RRULE'); + var rules = this.component.getAllProperties('rrule'); var i = 0; var len = rules.length; var result = Object.create(null); for (; i < len; i++) { - result[rules[i].data.FREQ] = true; + var value = rules[i].getFirstValue(); + result[value.freq] = true; } return result; }, get uid() { - return this._firstPropsValue('UID'); + return this._firstProp('uid'); }, set uid(value) { - this._setProp('UID', value); + this._setProp('uid', value); }, get startDate() { - return this._firstProp('DTSTART'); + return this._firstProp('dtstart'); }, set startDate(value) { - this._setProp('DTSTART', value); + this._setProp('dtstart', value); }, get endDate() { - return this._firstProp('DTEND'); + return this._firstProp('dtend'); }, set endDate(value) { - this._setProp('DTEND', value); + this._setProp('dtend', value); }, get duration() { @@ -5706,57 +6453,57 @@ ICAL.Event = (function() { }, get location() { - return this._firstPropsValue('LOCATION'); + return this._firstProp('location'); }, set location(value) { - return this._setProp('LOCATION', value); + return this._setProp('location', value); }, get attendees() { //XXX: This is way lame we should have a better // data structure for this later. - return this.component.getAllProperties('ATTENDEE'); + return this.component.getAllProperties('attendee'); }, get summary() { - return this._firstPropsValue('SUMMARY'); + return this._firstProp('summary'); }, set summary(value) { - this._setProp('SUMMARY', value); + this._setProp('summary', value); }, get description() { - return this._firstPropsValue('DESCRIPTION'); + return this._firstProp('description'); }, set description(value) { - this._setProp('DESCRIPTION', value); + this._setProp('description', value); }, get organizer() { - return this._firstProp('ORGANIZER'); + return this._firstProp('organizer'); }, set organizer(value) { - this._setProp('ORGANIZER', value); + this._setProp('organizer', value); }, get sequence() { - return this._firstPropsValue('SEQUENCE'); + return this._firstProp('sequence'); }, set sequence(value) { - this._setProp('SEQUENCE', value); + this._setProp('sequence', value); }, get recurrenceId() { - return this._firstProp('RECURRENCE-ID'); + return this._firstProp('recurrence-id'); }, set recurrenceId(value) { - this._setProp('RECURRENCE-ID', value); + this._setProp('recurrence-id', value); }, _setProp: function(name, value) { @@ -5767,21 +6514,6 @@ ICAL.Event = (function() { return this.component.getFirstPropertyValue(name); }, - /** - * Return the first property value. - * Most useful in cases where no properties - * are expected and the value will be a text type. - */ - _firstPropsValue: function(name) { - var prop = this._firstProp(name); - - if (prop && prop.data && prop.data.value) { - return prop.data.value[0]; - } - - return null; - }, - toString: function() { return this.component.toString(); } @@ -5890,11 +6622,11 @@ ICAL.ComponentParser = (function() { process: function(ical) { //TODO: this is sync now in the future we will have a incremental parser. if (typeof(ical) === 'string') { - ical = ICAL.parse(ical); + ical = ICAL.parse(ical)[1]; } - if (!(ical instanceof ICAL.icalcomponent)) { - ical = new ICAL.icalcomponent(ical); + if (!(ical instanceof ICAL.Component)) { + ical = new ICAL.Component(ical); } var components = ical.getAllSubcomponents(); @@ -5906,7 +6638,7 @@ ICAL.ComponentParser = (function() { component = components[i]; switch (component.name) { - case 'VEVENT': + case 'vevent': if (this.parseEvent) { this.onevent(new ICAL.Event(component)); } @@ -5925,53 +6657,4 @@ ICAL.ComponentParser = (function() { return ComponentParser; }()); -/* 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 = {} : ''; - -(function() { - ICAL.foldLength = 75; - ICAL.newLineChar = "\n"; - - /** - * Return a parsed ICAL object to the ICAL format. - * - * @param {Object} object parsed ical string. - * @return {String} ICAL string. - */ - ICAL.stringify = function ICALStringify(object) { - return ICAL.serializer.serializeToIcal(object); - }; - - /** - * Parse an ICAL object or string. - * - * @param {String|Object} ical ical string or pre-parsed object. - * @param {Boolean} decorate when true decorates object data types. - * - * @return {Object|ICAL.icalcomponent} The raw data or decorated icalcomponent. - */ - ICAL.parse = function ICALParse(ical) { - var state = ICAL.helpers.initState(ical, 0); - - while (state.buffer.length) { - var line = ICAL.helpers.unfoldline(state); - var lexState = ICAL.helpers.initState(line, state.lineNr); - if (line.match(/^\s*$/) && state.buffer.match(/^\s*$/)) { - break; - } - - var lineData = ICAL.icalparser.lexContentLine(lexState); - ICAL.icalparser.parseContentLine(state, lineData); - state.lineNr++; - } - - return state.currentData; - }; -}()); -- cgit