ical.js-one.com
Version:
[](http://travis-ci.org/mozilla-comm/ical.js)
1,875 lines (1,611 loc) • 188 kB
JavaScript
(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);