aboutsummaryrefslogtreecommitdiffstats
path: root/lib/caldav
diff options
context:
space:
mode:
authorJames Lal <james@lightsofapollo.com>2012-06-18 20:51:13 -0700
committerJames Lal <james@lightsofapollo.com>2012-06-18 20:51:13 -0700
commita6c747412c0960331e4055eee97d8328ff88d584 (patch)
treec4a82365e592a2d1cfa46cd0f5f595dde6626ff8 /lib/caldav
downloadjsCalDAV-a6c747412c0960331e4055eee97d8328ff88d584.tar.gz
Initial hack
Diffstat (limited to 'lib/caldav')
-rw-r--r--lib/caldav/caldav.js101
-rw-r--r--lib/caldav/ics.js140
-rw-r--r--lib/caldav/responder.js209
-rw-r--r--lib/caldav/sax.js158
-rw-r--r--lib/caldav/sax/propstat.js30
-rw-r--r--lib/caldav/xhr.js111
6 files changed, 749 insertions, 0 deletions
diff --git a/lib/caldav/caldav.js b/lib/caldav/caldav.js
new file mode 100644
index 0000000..cf81358
--- /dev/null
+++ b/lib/caldav/caldav.js
@@ -0,0 +1,101 @@
+(function(global, module) {
+
+ /**
+ * Define a list of paths
+ * this will only be used in the browser.
+ */
+ var paths = {};
+
+
+ /**
+ * Exports object is a shim
+ * we use in the browser to
+ * create an object that will behave much
+ * like module.exports
+ */
+ function Exports(path) {
+ this.path = path;
+ }
+
+ Exports.prototype = {
+
+ /**
+ * Unified require between browser/node.
+ * Path is relative to this file so you
+ * will want to use it like this from any depth.
+ *
+ *
+ * var Leaf = ns.require('sub/leaf');
+ *
+ *
+ * @param {String} path path lookup relative to this file.
+ */
+ require: function exportRequire(path) {
+ if (typeof(window) === 'undefined') {
+ return require(require('path').join(__dirname, path));
+ } else {
+ return paths[path];
+ }
+ },
+
+ /**
+ * Maps exports to a file path.
+ */
+ set exports(val) {
+ return paths[this.path] = val;
+ },
+
+ get exports() {
+ return paths[this.path];
+ }
+ };
+
+ /**
+ * Module object constructor.
+ *
+ *
+ * var module = Module('sub/leaf');
+ * module.exports = function Leaf(){}
+ *
+ *
+ * @constructor
+ * @param {String} path file path.
+ */
+ function Module(path) {
+ return new Exports(path);
+ }
+
+ Module.require = Exports.prototype.require;
+ Module.exports = Module;
+ Module._paths = paths;
+
+
+ /**
+ * Reference self as exports
+ * which also happens to be the constructor
+ * so you can assign items to the namespace:
+ *
+ * //assign to Module.X
+ * //assume module.exports is Module
+ * module.exports.X = Foo; //Module.X === Foo;
+ * Module.exports('foo'); //creates module.exports object.
+ *
+ */
+ module.exports = Module;
+
+ /**
+ * In the browser assign
+ * to a global namespace
+ * obviously 'Module' would
+ * be whatever your global namespace is.
+ */
+ if (this.window)
+ window.CalDav = Module;
+
+}(
+ this,
+ (typeof(module) === 'undefined') ?
+ {} :
+ module
+));
+
diff --git a/lib/caldav/ics.js b/lib/caldav/ics.js
new file mode 100644
index 0000000..47d9151
--- /dev/null
+++ b/lib/caldav/ics.js
@@ -0,0 +1,140 @@
+/**
+@namespace
+*/
+(function(module, ns) {
+
+ // Iterate over all entries if x is an array, otherwise just call fn on x.
+ function ForAll(x, fn) {
+ if (!(x instanceof Array)) {
+ fn(x);
+ return;
+ }
+ for (var n = 0; n < x.length; ++n)
+ fn(x[n]);
+ }
+
+ /* Pattern for an individual entry: name:value */
+ var ENTRY = /^([A-Za-z0-9-]+)((?:;[A-Za-z0-9-]+=(?:"[^"]+"|[^";:,]+)(?:,(?:"[^"]+"|[^";:,]+))*)*):(.*)$/;
+ /* Pattern for an individual parameter: name=value[,value] */
+ var PARAM = /;([A-Za-z0-9-]+)=((?:"[^"]+"|[^";:,]+)(?:,(?:"[^"]+"|[^";:,]+))*)/g;
+ /* Pattern for an individual parameter value: value | "value" */
+ var PARAM_VALUE = /,?("[^"]+"|[^";:,]+)/g;
+
+ // Parse a calendar in iCal format.
+ function ParseICal (text, success, error) {
+ // Parse the text into an object graph
+ var lines = text.replace('\r', '').split('\n');
+ var tos = Object.create(null);
+ var stack = [tos];
+
+ // Parse parameters for an entry. Foramt: <param>=<pvalue>[;...]
+ function parseParams(params) {
+ var map = Object.create(null);
+ var param = PARAM.exec(params);
+ while (param) {
+ var values = [];
+ var value = PARAM_VALUE.exec(param[2]);
+ while (value) {
+ values.push(value[1].replace(/^"(.*)"$/, '$1'));
+ value = PARAM_VALUE.exec(param[2]);
+ }
+ map[param[1].toLowerCase()] = (values.length > 1 ? values : values[0]);
+ param = PARAM.exec(params);
+ }
+ return map;
+ }
+
+ // Add a property to the current object. If a property with the same name
+ // already exists, turn it into an array.
+ function add(prop, value, params) {
+ if (params)
+ value = { parameters: parseParams(params), value: value };
+ if (prop in tos) {
+ var previous = tos[prop];
+ if (previous instanceof Array) {
+ previous.push(value);
+ return;
+ }
+ value = [previous, value];
+ }
+ tos[prop] = value;
+ }
+
+ for (var n = 0; n < lines.length; ++n) {
+ var line = lines[n];
+ // check whether the line continues (next line stats with space or tab)
+ var nextLine;
+ while ((nextLine = lines[n+1]) && (nextLine[0] == ' ' || nextLine[0] == '\t')) {
+ line += nextLine.substr(1);
+ ++n;
+ continue;
+ }
+ // parse the entry, format is 'PROPERTY:VALUE'
+ var matches = ENTRY.exec(line);
+ if (!matches)
+ return error('invalid format');
+ var prop = matches[1].toLowerCase();
+ var params = matches[2];
+ var value = matches[3];
+ switch (prop) {
+ case 'begin':
+ var obj = Object.create(null);
+ add(value.toLowerCase(), obj);
+ stack.push(tos = obj);
+ break;
+ case 'end':
+ stack.pop();
+ tos = stack[stack.length - 1];
+ if (stack.length == 1) {
+ var cal = stack[0];
+ if (typeof cal.vcalendar != 'object' || cal.vcalendar instanceof Array)
+ return error('single vcalendar object expected');
+ return success(cal.vcalendar);
+ }
+ break;
+ default:
+ add(prop, value, params);
+ break;
+ }
+ }
+ return error('unexpected end of file');
+ }
+
+ function Value(v) {
+ return (typeof v !== 'object') ? v : v.value;
+ }
+
+ function Parameter(v, name) {
+ if (typeof v !== 'object')
+ return undefined;
+ return v.parameters[name];
+ }
+
+ // Parse a time specification.
+ function ParseDateTime(v) {
+ var dt = Value(v);
+ if (Parameter(v, 'VALUE') == 'DATE') {
+ // 20081202
+ return new Date(dt.substr(0, 4), dt.substr(4, 2), dt.substr(6, 2));
+ }
+ v = Value(v);
+ // 20120426T130000Z
+ var year = dt.substr(0, 4);
+ var month = dt.substr(4, 2) - 1;
+ var day = dt.substr(6, 2);
+ var hour = dt.substr(9, 2);
+ var min = dt.substr(11, 2);
+ var sec = dt.substr(13, 2);
+ if (dt[15] == 'Z')
+ return new Date(Date.UTC(year, month, day, hour, min, sec));
+ return new Date(year, month, day, hour, min, sec);
+ }
+
+ module.exports = ParseICal;
+
+}.apply(
+ this,
+ (this.CalDav) ?
+ [CalDav('ics'), CalDav] :
+ [module, require('./caldav')]
+));
diff --git a/lib/caldav/responder.js b/lib/caldav/responder.js
new file mode 100644
index 0000000..b4abd21
--- /dev/null
+++ b/lib/caldav/responder.js
@@ -0,0 +1,209 @@
+/**
+@namespace
+*/
+(function(module, ns) {
+
+ /**
+ * Constructor
+ *
+ * @param {Object} list of events to add onto responder.
+ */
+ function Responder(events) {
+ this._$events = Object.create(null);
+
+ if (typeof(events) !== 'undefined') {
+ this.addEventListener(events);
+ }
+ };
+
+ /**
+ * Stringifies request to websocket
+ *
+ *
+ * @param {String} command command name.
+ * @param {Object} data object to be sent over the wire.
+ * @return {String} json object.
+ */
+ Responder.stringify = function stringify(command, data) {
+ return JSON.stringify([command, data]);
+ };
+
+ /**
+ * Parses request from WebSocket.
+ *
+ * @param {String} json json string to translate.
+ * @return {Object} ex: { event: 'test', data: {} }.
+ */
+ Responder.parse = function parse(json) {
+ var data;
+ try {
+ data = (json.forEach) ? json : JSON.parse(json);
+ } catch (e) {
+ throw new Error("Could not parse json: '" + json + '"');
+ }
+
+ return {event: data[0], data: data[1]};
+ };
+
+ Responder.prototype = {
+ parse: Responder.parse,
+ stringify: Responder.stringify,
+
+ /**
+ * Events on this instance
+ *
+ * @type Object
+ */
+ _$events: null,
+
+ /**
+ * Recieves json string event and dispatches an event.
+ *
+ * @param {String|Object} json data object to respond to.
+ * @param {String} json.event event to emit.
+ * @param {Object} json.data data to emit with event.
+ * @param {Object} [params] option number of params to pass to emit.
+ * @return {Object} result of WebSocketCommon.parse.
+ */
+ respond: function respond(json) {
+ var event = Responder.parse(json),
+ args = Array.prototype.slice.call(arguments).slice(1);
+
+ args.unshift(event.data);
+ args.unshift(event.event);
+
+ this.emit.apply(this, args);
+
+ return event;
+ },
+
+ //TODO: Extract event emitter logic
+
+ /**
+ * Adds an event listener to this object.
+ *
+ *
+ * @param {String} type event name.
+ * @param {Function} callback event callback.
+ */
+ addEventListener: function addEventListener(type, callback) {
+ var event;
+
+ if (typeof(callback) === 'undefined' && typeof(type) === 'object') {
+ for (event in type) {
+ if (type.hasOwnProperty(event)) {
+ this.addEventListener(event, type[event]);
+ }
+ }
+
+ return this;
+ }
+
+ if (!(type in this._$events)) {
+ this._$events[type] = [];
+ }
+
+ this._$events[type].push(callback);
+
+ return this;
+ },
+
+ /**
+ * Adds an event listener which will
+ * only fire once and then remove itself.
+ *
+ *
+ * @param {String} type event name.
+ * @param {Function} callback fired when event is emitted.
+ */
+ once: function once(type, callback) {
+ var self = this;
+ function onceCb() {
+ self.removeEventListener(type, onceCb);
+ callback.apply(this, arguments);
+ }
+
+ this.addEventListener(type, onceCb);
+
+ return this;
+ },
+
+ /**
+ * Emits an event.
+ *
+ * Accepts any number of additional arguments to pass unto
+ * event listener.
+ *
+ * @param {String} eventName name of the event to emit.
+ * @param {Object} [arguments] additional arguments to pass.
+ */
+ emit: function emit() {
+ var args = Array.prototype.slice.call(arguments),
+ event = args.shift(),
+ eventList,
+ self = this;
+
+ if (event in this._$events) {
+ eventList = this._$events[event];
+
+ eventList.forEach(function(callback) {
+ callback.apply(self, args);
+ });
+ }
+
+ return this;
+ },
+
+ /**
+ * Removes all event listeners for a given event type
+ *
+ *
+ * @param {String} event event type to remove.
+ */
+ removeAllEventListeners: function removeAllEventListeners(name) {
+ if (name in this._$events) {
+ //reuse array
+ this._$events[name].length = 0;
+ }
+
+ return this;
+ },
+
+ /**
+ * Removes a single event listener from a given event type
+ * and callback function.
+ *
+ *
+ * @param {String} eventName event name.
+ * @param {Function} callback same instance of event handler.
+ */
+ removeEventListener: function removeEventListener(name, callback) {
+ var i, length, events;
+
+ if (!(name in this._$events)) {
+ return false;
+ }
+
+ events = this._$events[name];
+
+ for (i = 0, length = events.length; i < length; i++) {
+ if (events[i] && events[i] === callback) {
+ events.splice(i, 1);
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ };
+
+ Responder.prototype.on = Responder.prototype.addEventListener;
+ module.exports = Responder;
+
+}.apply(
+ this,
+ (this.CalDav) ?
+ [CalDav('responder'), CalDav] :
+ [module, require('./caldav')]
+));
diff --git a/lib/caldav/sax.js b/lib/caldav/sax.js
new file mode 100644
index 0000000..28fa716
--- /dev/null
+++ b/lib/caldav/sax.js
@@ -0,0 +1,158 @@
+(function(module, ns) {
+
+ var sax = require('sax'),
+ Responder = ns.require('responder');
+
+ function Parser() {
+ var dispatch = [
+ 'onerror',
+ 'onopentag',
+ 'onclosetag',
+ 'ontext'
+ ];
+
+ this.parse = sax.parser(true, {
+ xmlns: true,
+ trim: true,
+ normalize: true,
+ lowercase: true
+ });
+
+ dispatch.forEach(function(event) {
+ this.parse[event] = this._dispatchEvent.bind(this, event);
+ }, this);
+
+ this.parse.onend = this.onend.bind(this);
+
+ this.depth = 0;
+ this.handles = {};
+
+ this.stack = [];
+ this.handlerStack = [];
+ this.tagStack = [];
+
+ this.current = this.root = {};
+ this.setParser(this);
+
+ Responder.call(this);
+ }
+
+ Parser.prototype = {
+
+ __proto__: Responder.prototype,
+
+ setParser: function(parse) {
+ this.currentParser = parse;
+ },
+
+ restoreParser: function() {
+ if ('oncomplete' in this.currentParser) {
+ this.currentParser.oncomplete.call(this);
+ }
+
+ var parser = this.handlerStack.pop();
+ this.setParser(parser || this);
+ },
+
+ _dispatchEvent: function(type, data) {
+
+ if (type === 'onopentag') {
+ data.tagSpec = data.uri + '/' + data.local;
+ }
+
+ if (type in this.currentParser) {
+ this.currentParser[type].call(this, data);
+ } else {
+ this[type](data);
+ }
+ },
+
+ addHandler: function(obj) {
+ this.handles[obj.tag] = obj;
+ },
+
+ checkHandler: function(handle) {
+ var handler,
+ handlers = this.currentParser.handles;
+
+ if (handle in handlers) {
+ handler = handlers[handle];
+ if (handler !== this.currentParser) {
+ return handler;
+ }
+ }
+
+ return false;
+ },
+
+ handleError: function() {
+ },
+
+ onopentag: function(data) {
+ var current = this.current,
+ name = data.local,
+ handler = this.checkHandler(data.tagSpec);
+
+ if (handler) {
+ this.handlerStack.push(this.currentParser);
+ this.setParser(handler);
+ return this._dispatchEvent('onopentag', data);
+ }
+
+ this.tagStack.push(data.tagSpec);
+ this.stack.push(this.current);
+
+ if (name in current) {
+ var next = {};
+
+ if (!(current[name] instanceof Array)) {
+ current[name] = [current[name]];
+ }
+
+ current[name].push(next);
+ this.current = next;
+ } else {
+ this.current = current[name] = {};
+ }
+ },
+
+ onend: function() {
+ this.emit('complete', this.root, this);
+ },
+
+ checkStackForHandler: function(restore) {
+ var stack = this.tagStack,
+ last = stack[stack.length - 1],
+ result;
+
+ result = last === this.currentParser.tag;
+
+ if (restore && result) {
+ this.restoreParser();
+ }
+
+ return result;
+ },
+
+ onclosetag: function() {
+ this.current = this.stack.pop();
+ this.tagStack.pop();
+ },
+
+ ontext: function(data) {
+ this.current.value = data;
+ },
+
+ write: function(data) {
+ return this.parse.write(data);
+ }
+ };
+
+ module.exports = Parser;
+
+}.apply(
+ this,
+ (this.CalDav) ?
+ [CalDav('xml_parser'), CalDav] :
+ [module, require('./caldav')]
+));
diff --git a/lib/caldav/sax/propstat.js b/lib/caldav/sax/propstat.js
new file mode 100644
index 0000000..0339f92
--- /dev/null
+++ b/lib/caldav/sax/propstat.js
@@ -0,0 +1,30 @@
+(function(module, ns) {
+ var Responder = ns.require('responder');
+
+ function Propstat(sax, complete) {
+
+ function onopen() {
+
+ }
+
+ function onclose() {
+
+ }
+
+ function ontext() {
+
+ }
+
+ sax.on('tagopen', onopen);
+ sax.on('tagclose', ontext);
+ sax.on('text', ontext);
+ }
+
+ module.exports = Propstat;
+
+}.apply(
+ this,
+ (this.CalDav) ?
+ [CalDav('sax/propstat'), CalDav] :
+ [module, require('../caldav')]
+));
diff --git a/lib/caldav/xhr.js b/lib/caldav/xhr.js
new file mode 100644
index 0000000..bfc3eeb
--- /dev/null
+++ b/lib/caldav/xhr.js
@@ -0,0 +1,111 @@
+/**
+@namespace
+*/
+(function(module, ns) {
+ var Native;
+
+ if (typeof(window) === 'undefined') {
+ Native = require('xmlhttprequest').XMLHttpRequest;
+ } else {
+ Native = window.XMLHttpRequest;
+ }
+
+ /**
+ * Creates a XHR wrapper.
+ * Depending on the platform this is loaded
+ * from the correct wrapper type will be used.
+ *
+ * Options are derived from properties on the prototype.
+ * See each property for its default value.
+ *
+ * @class
+ * @name CalDav.Xhr
+ * @param {Object} options options for xhr.
+ * @param {String} [options.method="GET"] any HTTP verb like 'GET' or 'POST'.
+ * @param {Boolean} [options.async] false will indicate
+ * a synchronous request.
+ * @param {Object} [options.headers] full of http headers.
+ * @param {Object} [options.data] post data.
+ */
+ function Xhr(options) {
+ var key;
+ if (typeof(options) === 'undefined') {
+ options = {};
+ }
+
+ for (key in options) {
+ if (options.hasOwnProperty(key)) {
+ this[key] = options[key];
+ }
+ }
+ }
+
+ Xhr.prototype = {
+ /** @scope CalDav.Xhr.prototype */
+
+ xhrClass: Native,
+ method: 'GET',
+ async: true,
+ waiting: false,
+ user: null,
+ password: null,
+
+ headers: {},
+ data: {},
+
+ _seralize: function _seralize() {
+ return this.data;
+ },
+
+ /**
+ * Aborts request if its in progress.
+ */
+ abort: function abort() {
+ if (this.xhr) {
+ this.xhr.abort();
+ }
+ },
+
+ /**
+ * Sends request to server.
+ *
+ * @param {Function} callback success/failure handler.
+ */
+ send: function send(callback) {
+ var header, xhr;
+
+ if (typeof(callback) === 'undefined') {
+ callback = this.callback;
+ }
+
+ xhr = this.xhr = new this.xhrClass();
+ xhr.open(this.method, this.url, this.async, this.user, this.password);
+
+ for (header in this.headers) {
+ if (this.headers.hasOwnProperty(header)) {
+ xhr.setRequestHeader(header, this.headers[header]);
+ }
+ }
+
+ xhr.onreadystatechange = function onReadyStateChange() {
+ var data;
+ if (xhr.readyState === 4) {
+ data = xhr.responseText;
+ this.waiting = false;
+ callback(data, xhr);
+ }
+ }.bind(this);
+
+ this.waiting = true;
+ xhr.send(this._seralize());
+ }
+ };
+
+ module.exports = Xhr;
+
+}.apply(
+ this,
+ (this.CalDav) ?
+ [CalDav('xhr'), CalDav] :
+ [module, require('./caldav')]
+));