ical.js-one.com
Version:
[](http://travis-ci.org/mozilla-comm/ical.js)
350 lines (292 loc) • 9.05 kB
JavaScript
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 lastParamIndex;
var lastValuePos;
// name of property or begin/end
var name;
var value;
// params is only overridden if paramPos !== -1.
// we can't do params = params || {} later on
// because it sacrifices ops.
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
*/
// when the parameter delimiter is after the
// value delimiter then its not a parameter.
if ((paramPos !== -1 && valuePos !== -1)) {
// when the parameter delimiter is after the
// value delimiter then its not a parameter.
if (paramPos > valuePos) {
paramPos = -1;
}
}
var parsedParams;
if (paramPos !== -1) {
name = line.substring(0, paramPos).toLowerCase();
parsedParams = parser._parseParameters(line.substring(paramPos), 0);
params = parsedParams[0];
lastParamIndex = parsedParams[1].length + parsedParams[2] + paramPos;
if ((lastValuePos =
line.substring(lastParamIndex).indexOf(VALUE_DELIMITER)) !== -1) {
value = line.substring(lastParamIndex + lastValuePos + 1);
}
} else if (valuePos !== -1) {
// without parmeters (BEGIN:VCAENDAR, CLASS:PUBLIC)
name = line.substring(0, valuePos).toLowerCase();
value = line.substring(valuePos + 1);
if (name === 'begin') {
var newComponent = [value.toLowerCase(), [], []];
if (state.stack.length === 1) {
state.component.push(newComponent);
} else {
state.component[2].push(newComponent);
}
state.stack.push(state.component);
state.component = newComponent;
return;
} else 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);
}
}
// 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 = {};
var name;
var value;
var type;
// 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) {
name = line.substr(lastParam + 1, pos - lastParam - 1);
var nextChar = line[pos + 1];
if (nextChar === '"') {
var valuePos = pos + 2;
pos = helpers.unescapedIndexOf(line, '"', valuePos);
value = line.substr(valuePos, pos - valuePos);
lastParam = helpers.unescapedIndexOf(line, PARAM_DELIMITER, pos);
} else {
var valuePos = pos + 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);
if (nextPos === -1) {
nextPos = line.length;
}
pos = false;
} else {
lastParam = nextPos;
}
value = line.substr(valuePos, nextPos - valuePos);
}
if (name in design.param && design.param[name].valueType) {
type = design.param[name].valueType;
} else {
type = DEFAULT_TYPE;
}
result[name.toLowerCase()] = parser._parseValue(value, type);
}
return [result, value, valuePos];
}
/**
* 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;
}());