From 5e731c14d9fca4e99ac73f020d69008431ef4f81 Mon Sep 17 00:00:00 2001 From: James Lal Date: Wed, 27 Jun 2012 16:58:05 +0200 Subject: working calendar queries --- lib/webcals/ical.js | 139 ++++++++++++++++++++++++ lib/webcals/ics.js | 140 ------------------------- lib/webcals/request/calendar_query.js | 23 +++- lib/webcals/sax/dav_response.js | 21 +++- lib/webcals/templates/calendar_data.js | 6 ++ lib/webcals/templates/calendar_filter.js | 4 +- samples/xml/propget.xml | 3 +- samples/xml/req_calendar_query.xml | 15 +++ test/webcals/ical_test.js | 11 ++ test/webcals/ics_test.js | 11 -- test/webcals/request/calendar_query_test.js | 23 ++-- test/webcals/request/propfind_test.js | 2 - test/webcals/sax/dav_response_test.js | 2 + test/webcals/templates/calendar_data_test.js | 9 +- test/webcals/templates/calendar_filter_test.js | 8 +- 15 files changed, 242 insertions(+), 175 deletions(-) create mode 100644 lib/webcals/ical.js delete mode 100644 lib/webcals/ics.js create mode 100644 samples/xml/req_calendar_query.xml create mode 100644 test/webcals/ical_test.js delete mode 100644 test/webcals/ics_test.js diff --git a/lib/webcals/ical.js b/lib/webcals/ical.js new file mode 100644 index 0000000..45cef83 --- /dev/null +++ b/lib/webcals/ical.js @@ -0,0 +1,139 @@ +(function(module, ns) { + // Credit: Andreas Gal - I removed the callback / xhr logic + + // Iterate over all entries if x is an array, otherwise just call fn on x. + + /* 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) { + // Parse the text into an object graph + var lines = text.replace(/\r/g, '').split('\n'); + var tos = Object.create(null); + var stack = [tos]; + + // Parse parameters for an entry. Foramt: =[;...] + 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) { + throw new 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) { + throw new Error('single vcalendar object expected'); + } + + return cal.vcalendar; + } + break; + default: + add(prop, value, params); + break; + } + } + throw new 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.Webcals) ? + [Webcals('ics'), Webcals] : + [module, require('./webcals')] +)); + diff --git a/lib/webcals/ics.js b/lib/webcals/ics.js deleted file mode 100644 index 91f549b..0000000 --- a/lib/webcals/ics.js +++ /dev/null @@ -1,140 +0,0 @@ -/** -@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: =[;...] - 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.Webcals) ? - [Webcals('ics'), Webcals] : - [module, require('./webcals')] -)); diff --git a/lib/webcals/request/calendar_query.js b/lib/webcals/request/calendar_query.js index b39853f..b9a350d 100644 --- a/lib/webcals/request/calendar_query.js +++ b/lib/webcals/request/calendar_query.js @@ -2,6 +2,7 @@ var Propfind = ns.require('request/propfind'); var CalendarData = ns.require('templates/calendar_data'); + var CalendarFilter = ns.require('templates/calendar_filter'); /** * Creates a calendar query request. @@ -17,16 +18,30 @@ this.xhr.headers['Depth'] = this.depth || 1; this.xhr.method = 'REPORT'; this.fields = new CalendarData(); - this.template.rootTag = 'calendar-query'; + this.filters = new CalendarFilter(); + + this.template.rootTag = ['caldav', 'calendar-query']; } CalendarQuery.prototype = { __proto__: Propfind.prototype, _createPayload: function() { - var props = this._props.join(''); - props += this.fields.render(this.template); - var content = this.template.tag('prop', props); + var content; + var props; + + props = this._props.join(''); + + if (this.fields) { + props += this.fields.render(this.template); + } + + content = this.template.tag('prop', props); + + if (this.filters) { + content += this.filters.render(this.template); + } + return this.template.render(content); } diff --git a/lib/webcals/sax/dav_response.js b/lib/webcals/sax/dav_response.js index 638951e..26d3453 100644 --- a/lib/webcals/sax/dav_response.js +++ b/lib/webcals/sax/dav_response.js @@ -3,6 +3,7 @@ var HTTP_STATUS = /([0-9]{3,3})/; var Base = ns.require('sax/base'); + var ParseICal = ns.require('ical'); var TextHandler = Base.create({ name: 'text', @@ -21,6 +22,24 @@ } }); + var CalendarDataHandler = Base.create({ + name: 'calendar data', + + //don't add text only elements + //to the stack as objects + onopentag: null, + onclosetag: null, + + //add the value to the parent + //value where key is local tag name + //and value is the text. + ontext: function(data) { + var handler = this.handler; + this.current[this.currentTag[handler.tagField]] = ParseICal(data); + } + }); + + var HrefHandler = Base.create({ name: 'href', @@ -108,6 +127,7 @@ 'DAV:/status': HttpStatusHandler, 'DAV:/resourcetype': ArrayHandler, 'DAV:/principal-URL': HrefHandler, + 'urn:ietf:params:xml:ns:caldav/calendar-data': CalendarDataHandler, 'DAV:/value': TextHandler, 'urn:ietf:params:xml:ns:caldav/calendar-home-set': HrefHandler, 'urn:ietf:params:xml:ns:caldav/calendar-user-address-set': HrefHandler @@ -186,4 +206,3 @@ [Webcals('sax/dav_response'), Webcals] : [module, require('../webcals')] )); - diff --git a/lib/webcals/templates/calendar_data.js b/lib/webcals/templates/calendar_data.js index a0c0938..a652303 100644 --- a/lib/webcals/templates/calendar_data.js +++ b/lib/webcals/templates/calendar_data.js @@ -1,6 +1,7 @@ (function(module, ns) { function CalendarData() { + this._hasItems = false; this.struct = {}; } @@ -18,6 +19,7 @@ */ select: function(type, list) { var struct = this.struct; + this._hasItems = true; if (!(type in struct)) { struct[type] = []; @@ -76,6 +78,10 @@ * @return {String} xml output. */ render: function(template) { + if (!this._hasItems) { + return template.tag(['caldav', this.rootName]); + } + var struct = this.struct; var output = template.tag( ['caldav', this.rootName], diff --git a/lib/webcals/templates/calendar_filter.js b/lib/webcals/templates/calendar_filter.js index 15cfc8c..05bd742 100644 --- a/lib/webcals/templates/calendar_filter.js +++ b/lib/webcals/templates/calendar_filter.js @@ -10,7 +10,7 @@ __proto__: CalendarData.prototype, - filter: CalendarData.prototype.select, + add: CalendarData.prototype.select, compName: 'comp-filter', rootName: 'filter' @@ -21,7 +21,7 @@ }.apply( this, (this.Webcals) ? - [Webcals('templates/calendar_data'), Webcals] : + [Webcals('templates/calendar_filter'), Webcals] : [module, require('../webcals')] )); diff --git a/samples/xml/propget.xml b/samples/xml/propget.xml index c8487b2..2db1aa2 100644 --- a/samples/xml/propget.xml +++ b/samples/xml/propget.xml @@ -1,5 +1,5 @@ - + @@ -40,5 +40,4 @@ - diff --git a/samples/xml/req_calendar_query.xml b/samples/xml/req_calendar_query.xml new file mode 100644 index 0000000..e983706 --- /dev/null +++ b/samples/xml/req_calendar_query.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + diff --git a/test/webcals/ical_test.js b/test/webcals/ical_test.js new file mode 100644 index 0000000..ed9af4a --- /dev/null +++ b/test/webcals/ical_test.js @@ -0,0 +1,11 @@ +var fs = require('fs'), + ics = requireLib('ical'), + data = fs.readFileSync(__dirname + '/../../data/test.data', 'utf8'); + +suite('webcals/ics', function() { + + test('intiailizer', function() { + assert.ok(ics); + }); + +}); diff --git a/test/webcals/ics_test.js b/test/webcals/ics_test.js deleted file mode 100644 index af1192e..0000000 --- a/test/webcals/ics_test.js +++ /dev/null @@ -1,11 +0,0 @@ -var fs = require('fs'), - ics = requireLib('ics'), - data = fs.readFileSync(__dirname + '/../../data/test.data', 'utf8'); - -suite('webcals/ics', function() { - - test('intiailizer', function() { - assert.ok(ics); - }); - -}); diff --git a/test/webcals/request/calendar_query_test.js b/test/webcals/request/calendar_query_test.js index f960e15..b1a167d 100644 --- a/test/webcals/request/calendar_query_test.js +++ b/test/webcals/request/calendar_query_test.js @@ -2,11 +2,12 @@ requireRequest(); requireLib('request/propfind'); requireLib('request/calendar_query'); -suite('webcals/request/propfind', function() { +suite('webcals/request/calendar_query', function() { var Propfind, - CalendarData, FakeXhr, + CalendarData, CalendarQuery, + CalendarFilter, Xhr, Template, oldXhrClass, @@ -18,6 +19,7 @@ suite('webcals/request/propfind', function() { suiteSetup(function() { Propfind = Webcals.require('request/propfind'); CalendarData = Webcals.require('templates/calendar_data'); + CalendarFilter = Webcals.require('templates/calendar_filter'); CalendarQuery = Webcals.require('request/calendar_query'); SaxResponse = Webcals.require('sax/dav_response'); FakeXhr = Webcals.require('support/fake_xhr'); @@ -44,13 +46,15 @@ suite('webcals/request/propfind', function() { assert.equal(subject.xhr.method, 'REPORT'); assert.instanceOf(subject.fields, CalendarData); + assert.instanceOf(subject.filters, CalendarFilter); }); test('#_createPayload', function() { subject.prop('getetag'); subject.fields.select('VEVENT', ['NAME']); + subject.filters.add('VEVENT', true); - var tags = [ + var props = [ '', '', '', @@ -61,12 +65,19 @@ suite('webcals/request/propfind', function() { '' ].join(''); + var filter = [ + '', + '', + '' + ].join(''); + var expected = [ subject.template.doctype, - '', - '', tags, '', - '' + '', props, '', + '', filter, '', + '' ].join(''); assert.equal(subject._createPayload(), expected); diff --git a/test/webcals/request/propfind_test.js b/test/webcals/request/propfind_test.js index 6d2dbe7..1f152b0 100644 --- a/test/webcals/request/propfind_test.js +++ b/test/webcals/request/propfind_test.js @@ -72,8 +72,6 @@ suite('webcals/request/propfind', function() { var result = subject._createPayload(); - console.log(result); - assert.equal(subject._createPayload(), expected); }); diff --git a/test/webcals/sax/dav_response_test.js b/test/webcals/sax/dav_response_test.js index 1279b84..f91fcef 100644 --- a/test/webcals/sax/dav_response_test.js +++ b/test/webcals/sax/dav_response_test.js @@ -1,6 +1,7 @@ requireLib('sax'); requireLib('sax/base'); requireLib('sax/dav_response'); +requireLib('ical'); suite('webcals/sax/dav_response', function() { @@ -61,6 +62,7 @@ suite('webcals/sax/dav_response', function() { value: {} } } + }; test('output', function(done) { diff --git a/test/webcals/templates/calendar_data_test.js b/test/webcals/templates/calendar_data_test.js index 5223f18..1828b39 100644 --- a/test/webcals/templates/calendar_data_test.js +++ b/test/webcals/templates/calendar_data_test.js @@ -52,11 +52,16 @@ suite('webcals/templates/calendar_data', function() { '' ].join(''); - setup(function() { - select(); + test('without items', function() { + var output = subject.render(template); + assert.equal( + output, + '' + ); }); test('output', function() { + select(); var output = subject.render(template); assert.equal(output, expected); }); diff --git a/test/webcals/templates/calendar_filter_test.js b/test/webcals/templates/calendar_filter_test.js index 517e78a..d79883b 100644 --- a/test/webcals/templates/calendar_filter_test.js +++ b/test/webcals/templates/calendar_filter_test.js @@ -1,7 +1,8 @@ requireRequest(); requireLib('templates/calendar_data'); +requireLib('templates/calendar_filter'); -suite('webcals/templates/calendar_data', function() { +suite('webcals/templates/calendar_filter', function() { var CalendarFilter; var Template; @@ -9,7 +10,7 @@ suite('webcals/templates/calendar_data', function() { var template; function filter() { - subject.filter('VEVENT', true); + subject.add('VEVENT', true); } suiteSetup(function() { @@ -44,9 +45,6 @@ suite('webcals/templates/calendar_data', function() { test('output', function() { var output = subject.render(template); - console.log(); - console.log(output) - console.log(); assert.equal(output, expected); }); }); -- cgit