diff options
Diffstat (limited to 'lib')
-rw-r--r-- | lib/caldav/caldav.js | 101 | ||||
-rw-r--r-- | lib/caldav/ics.js | 140 | ||||
-rw-r--r-- | lib/caldav/responder.js | 209 | ||||
-rw-r--r-- | lib/caldav/sax.js | 158 | ||||
-rw-r--r-- | lib/caldav/sax/propstat.js | 30 | ||||
-rw-r--r-- | lib/caldav/xhr.js | 111 |
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')] +)); |