diff options
author | James Lal <james@lightsofapollo.com> | 2012-06-19 14:01:01 -0700 |
---|---|---|
committer | James Lal <james@lightsofapollo.com> | 2012-06-19 14:01:01 -0700 |
commit | 936e76b8945c6f310ad893905e08728715620e38 (patch) | |
tree | 39c969cb8739e26e3c4f628b50f1afffa8330dcc | |
parent | f1ae9e6f2a4503b9c5ab55abb13d95cbd9ee753c (diff) | |
download | jsCalDAV-936e76b8945c6f310ad893905e08728715620e38.tar.gz |
v1 sax parser
-rw-r--r-- | Makefile | 1 | ||||
-rw-r--r-- | lib/webcals/sax.js | 229 | ||||
-rw-r--r-- | lib/webcals/sax/base.js | 73 | ||||
-rw-r--r-- | lib/webcals/sax/propstat.js | 30 | ||||
-rw-r--r-- | samples/xml/complex-tree.xml | 24 | ||||
-rw-r--r-- | samples/xml/propstat-success.xml | 10 | ||||
-rw-r--r-- | samples/xml/simple.xml | 6 | ||||
-rw-r--r-- | test/helper.js | 18 | ||||
-rw-r--r-- | test/webcals/ics_test.js | 6 | ||||
-rw-r--r-- | test/webcals/sax/base_test.js | 94 | ||||
-rw-r--r-- | test/webcals/sax/propstat_test.js | 27 | ||||
-rw-r--r-- | test/webcals/sax_test.js | 387 |
12 files changed, 678 insertions, 227 deletions
@@ -6,6 +6,7 @@ test: --ui tdd \ --reporter $(REPORTER) \ --growl test/helper.js \ + test/webcals/sax/*_test.js \ test/webcals/*_test.js .PHONY: watch diff --git a/lib/webcals/sax.js b/lib/webcals/sax.js index 1e169fd..9cc8425 100644 --- a/lib/webcals/sax.js +++ b/lib/webcals/sax.js @@ -1,38 +1,54 @@ (function(module, ns) { - var sax = require('sax'), - Responder = ns.require('responder'); + var Responder = ns.require('responder'); - function Parser() { - var dispatch = [ - 'onerror', + if (typeof(sax) === 'undefined') { + Parser.sax = require('sax'); + } else { + Parser.sax = sax; + } + + /** + * Creates a parser object. + * + * @param {Object} baseHandler base sax handler. + */ + function Parser(baseHandler) { + var handler; + + var events = [ + 'ontext', 'onopentag', 'onclosetag', - 'ontext' + 'onerror', + 'onend' ]; - this.parse = sax.parser(true, { + if (typeof(baseHandler) !== 'undefined') { + handler = baseHandler; + } else { + handler = ns.require('sax/base'); + } + + this.stack = []; + this.handles = {}; + this._handlerStack = []; + this.tagStack = []; + this.root = this.current = {}; + + this.setHandler(handler); + + this._parse = Parser.sax.parser(true, { xmlns: true, trim: true, - normalize: true, + normalize: false, lowercase: true }); - dispatch.forEach(function(event) { - this.parse[event] = this._dispatchEvent.bind(this, event); + events.forEach(function(event) { + this._parse[event] = this[event].bind(this); }, this); - this.parse.onend = this.onend.bind(this); - - this.handles = {}; - - this.stack = []; - this.handlerStack = []; - this.tagStack = []; - - this.current = this.root = {}; - this.setParser(this); - Responder.call(this); } @@ -40,43 +56,73 @@ __proto__: Responder.prototype, - setParser: function(parse) { - this.currentParser = parse; - }, - - restoreParser: function() { - if ('oncomplete' in this.currentParser) { - this.currentParser.oncomplete.call(this); + /** + * Sets current handler, optionally adding + * previous one to the handlerStack. + * + * @param {Object} handler new handler. + * @param {Boolean} storeOriginal store old handler? + */ + setHandler: function(handler, storeOriginal) { + if (storeOriginal) { + this._handlerStack.push(this.handler); } - var parser = this.handlerStack.pop(); - this.setParser(parser || this); + this.handler = handler; }, - _dispatchEvent: function(type, data) { - - if (type === 'onopentag') { - data.tagSpec = data.uri + '/' + data.local; + /** + * Sets handler to previous one in the stack. + */ + restoreHandler: function() { + if (this._handlerStack.length) { + this.handler = this._handlerStack.pop(); } + }, - if (type in this.currentParser) { - this.currentParser[type].call(this, data); - } else { - this[type](data); - } + /** + * Registers a top level handler + * + * @param {String} tag xmlns uri/local tag name for example + * DAV:/a. + * + * @param {Object} handler new handler to use when tag is + * triggered. + */ + registerHandler: function(tag, handler) { + this.handles[tag] = handler; + }, + + /** + * Writes data into the parser. + * + * @param {String} chunk partial/complete chunk of xml. + */ + write: function(chunk) { + return this._parse.write(chunk); }, - addHandler: function(obj) { - this.handles[obj.tag] = obj; + get closed() { + return this._parse.closed; }, - checkHandler: function(handle) { - var handler, - handlers = this.currentParser.handles; + /** + * Determines if given tagSpec has a specific handler. + * + * @param {String} tagSpec usual tag spec. + */ + getHandler: function(tagSpec) { + var handler; + var handlers = this.handler.handles; + + if (!handlers) { + handlers = this.handles; + } + + if (tagSpec in handlers) { + handler = handlers[tagSpec]; - if (handle in handlers) { - handler = handlers[handle]; - if (handler !== this.currentParser) { + if (handler !== this.handler) { return handler; } } @@ -84,66 +130,75 @@ return false; }, - handleError: function() { + _fireHandler: function(event, data) { + if (event in this.handler) { + this.handler[event].call(this, data, this.handler); + } }, 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); - } + var handle; + var stackData = {}; - this.tagStack.push(data.tagSpec); - this.stack.push(this.current); + //build tagSpec for others to use. + data.tagSpec = data.uri + '/' + data.local; - if (name in current) { - var next = {}; + //add to stackData + stackData.tag = data.tagSpec; - if (!(current[name] instanceof Array)) { - current[name] = [current[name]]; - } + // shortcut to the current tag object + this.currentTag = data; + + //determine if we need to switch to another + //handler object. + handle = this.getHandler(data.tagSpec); - current[name].push(next); - this.current = next; - } else { - this.current = current[name] = {}; + if (handle) { + //switch to new handler object + this.setHandler(handle, true); + stackData.handler = handle; } - }, - onend: function() { - this.emit('complete', this.root, this); + this.tagStack.push(stackData); + this._fireHandler('onopentag', data); }, - checkStackForHandler: function(restore) { - var stack = this.tagStack, - last = stack[stack.length - 1], - result; + onclosetag: function(data) { + var stack, handler; - result = last === this.currentParser.tag; + stack = this.tagStack[this.tagStack.length - 1]; - if (restore && result) { - this.restoreParser(); + if (stack.handler) { + //fire oncomplete handler if available + this._fireHandler('oncomplete'); } - return result; - }, + //fire the onclosetag event + this._fireHandler('onclosetag', data); - onclosetag: function() { - this.current = this.stack.pop(); + if (stack.handler) { + //restore previous handler + this.restoreHandler(); + } + + //actually remove the stack tag this.tagStack.pop(); }, ontext: function(data) { - this.current.value = data; + this._fireHandler('ontext', data); }, - write: function(data) { - return this.parse.write(data); + onerror: function(data) { + //TODO: XXX implement handling of parsing errors. + //unlikely but possible if server goes down + //or there is some authentication issue that + //we miss. + this._fireHandler('onerror', data); + }, + + onend: function() { + this._fireHandler('onend', this.root); } }; @@ -151,7 +206,7 @@ }.apply( this, - (this.CalDav) ? - [CalDav('xml_parser'), CalDav] : + (this.Webcals) ? + [Webcals('xml_parser'), Webcals] : [module, require('./webcals')] )); diff --git a/lib/webcals/sax/base.js b/lib/webcals/sax/base.js new file mode 100644 index 0000000..c62981f --- /dev/null +++ b/lib/webcals/sax/base.js @@ -0,0 +1,73 @@ +(function(module, ns) { + + var Base = { + + name: 'base', + + tagField: 'local', + + /** + * Creates a new object with base as its prototype. + * Adds ._super to object as convenience prop to access + * the parents functions. + * + * @param {Object} obj function overrides. + * @return {Object} new object. + */ + create: function(obj) { + var key; + var child = Object.create(this); + + child._super = this; + + for (key in obj) { + if (obj.hasOwnProperty(key)) { + child[key] = obj[key]; + } + } + + return child; + }, + + onopentag: function(data, handler) { + var current = this.current; + var name = data[handler.tagField]; + + 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] = {}; + } + }, + + ontext: function(data) { + this.current.value = data; + }, + + onclosetag: function() { + this.current = this.stack.pop(); + }, + + onend: function() { + this.emit('complete', this.root); + } + }; + + module.exports = Base; + +}.apply( + this, + (this.Webcals) ? + [Webcals('sax/base'), Webcals] : + [module, require('../webcals')] +)); diff --git a/lib/webcals/sax/propstat.js b/lib/webcals/sax/propstat.js deleted file mode 100644 index 03bedaa..0000000 --- a/lib/webcals/sax/propstat.js +++ /dev/null @@ -1,30 +0,0 @@ -(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('../webcals')] -)); diff --git a/samples/xml/complex-tree.xml b/samples/xml/complex-tree.xml new file mode 100644 index 0000000..6b599be --- /dev/null +++ b/samples/xml/complex-tree.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="UTF-8"?> +<D:complex xmlns:D="DAV:"> + <D:response> + </D:response> + + <D:response> + <D:href>uri</D:href> + + <D:propstat> + <D:status>400</D:status> + <D:prop> + <A:current xmlns:A="DAV:" /> + </D:prop> + </D:propstat> + + <D:propstat> + <D:status>200</D:status> + <D:prop> + <A:next xmlns:A="DAV:" /> + </D:prop> + </D:propstat> + </D:response> +</D:complex> + diff --git a/samples/xml/propstat-success.xml b/samples/xml/propstat-success.xml deleted file mode 100644 index 93f27ec..0000000 --- a/samples/xml/propstat-success.xml +++ /dev/null @@ -1,10 +0,0 @@ -<D:status>HTTP/1.1 200 OK</D:status> -<D:prop> - <D:principal-URL> - <D:href>/calendar/dav/calmozilla1@gmail.com/user/</D:href> - </D:principal-URL> - <D:resourcetype> - <D:principal /> - <D:collection /> - </D:resourcetype> -</D:prop> diff --git a/samples/xml/simple.xml b/samples/xml/simple.xml new file mode 100644 index 0000000..0e539da --- /dev/null +++ b/samples/xml/simple.xml @@ -0,0 +1,6 @@ +<D:simple xmlns:D="DAV:"> + <D:a>Foo</D:a> + <D:a href="some-url">Foo + bar</D:a> + <D:div><D:span>span</D:span></D:div> +</D:simple> diff --git a/test/helper.js b/test/helper.js index b36aeeb..d5ac7a0 100644 --- a/test/helper.js +++ b/test/helper.js @@ -4,10 +4,6 @@ var chai = require('chai'), chai.Assertion.includeStack = true; assert = chai.assert; -globalNS = { - require: require -}; - loadSample = function(file, cb) { var root = __dirname + '/../samples/'; fs.readFile(root + file, 'utf8', function(err, contents) { @@ -15,6 +11,20 @@ loadSample = function(file, cb) { }); }; +defineSample = function(file, cb) { + suiteSetup(function(done) { + loadSample(file, function(err, data) { + if (err) { + done(err); + } + cb(data); + done(); + }); + }); +}; + requireLib = function(lib) { return require(__dirname + '/../lib/webcals/' + lib); }; + +Webcals = require('../lib/webcals/webcals.js'); diff --git a/test/webcals/ics_test.js b/test/webcals/ics_test.js index 904bbf9..af1192e 100644 --- a/test/webcals/ics_test.js +++ b/test/webcals/ics_test.js @@ -5,11 +5,7 @@ var fs = require('fs'), suite('webcals/ics', function() { test('intiailizer', function() { - ics(data, function(data) { - console.log(data.vevent[0]); - }, function() { - console.log('y'); - }) + assert.ok(ics); }); }); diff --git a/test/webcals/sax/base_test.js b/test/webcals/sax/base_test.js new file mode 100644 index 0000000..9ad0bdc --- /dev/null +++ b/test/webcals/sax/base_test.js @@ -0,0 +1,94 @@ +requireLib('sax'); +requireLib('sax/base'); + +suite('webcals/sax/base', function() { + + var data, + subject, + parser, + Parse, + Base, + handler; + + + suiteSetup(function() { + Parse = Webcals.require('sax'); + Base = Webcals.require('sax/base'); + }); + + setup(function() { + //we omit the option to pass base parser + //because we are the base parser + subject = new Parse(); + }); + + test('#create', function() { + function childText() {} + + var Child = Base.create({ + ontext: childText + }); + + assert.equal(Child.ontext, childText); + assert.equal(Child.tagField, Base.tagField); + + assert.isTrue( + Base.isPrototypeOf(Child), + 'should have base in proto chain' + ); + + assert.equal(Child._super, Base); + + var ChildChild = Child.create(); + + assert.isTrue( + Child.isPrototypeOf(ChildChild), + 'should have child in childchild protochain' + ); + + assert.isTrue( + Base.isPrototypeOf(ChildChild), + 'should have base in childchild protochain' + ); + + assert.equal(ChildChild._super, Child); + + }); + + suite('base parser', function() { + var xml; + + defineSample('xml/simple.xml', function(data) { + xml = data; + }); + + //base baser does not + //care about attrs at this point + expected = { + simple: { + a: [ + { value: 'Foo' }, + { value: 'Foo\n bar' } + ], + div: { + span: { + value: 'span' + } + } + } + }; + + test('simple tree', function(done) { + subject.once('complete', function(data) { + assert.deepEqual( + data, expected, + "expected \n '" + JSON.stringify(data) + "'\n to equal \n '" + + JSON.stringify(expected) + '\n"' + ); + done(); + }); + subject.write(xml).close(); + }); + }); + +}); diff --git a/test/webcals/sax/propstat_test.js b/test/webcals/sax/propstat_test.js deleted file mode 100644 index 56e65fd..0000000 --- a/test/webcals/sax/propstat_test.js +++ /dev/null @@ -1,27 +0,0 @@ -suite('webcals/sax/propstat', function() { - var stat = requireLib('sax/propstat'), - sax = requireLib('sax'), - subject; - - var expected = { - status: 200, - 'principal-URL': '/calendar/dav/calmozilla1@gmail.com/user/', - 'resource-type': [ - 'principal', - 'collection' - ] - }; - - test('propstat success', function(done) { - var parser = sax(); - - console.log(parser.on); - stat(parser, function(err, result) { - console.log(result); - done(); - }); - - parser.write(loadSample('xml/propstat-success.xml')).close(); - }); - -}); diff --git a/test/webcals/sax_test.js b/test/webcals/sax_test.js index 312990c..71f6571 100644 --- a/test/webcals/sax_test.js +++ b/test/webcals/sax_test.js @@ -1,94 +1,353 @@ -var xml = requireLib('sax'); +requireLib('sax'); +requireLib('sax/base'); -suite('sax test', function() { +suite('webcals/sax', function() { var data, - subject; + subject, + SAX, + Base, + handler; - test('existance', function() { - return; - var parser = new xml(); + // you should not use instances + // for handlers this is only + // to make testing easier. + function TestHander() { + this.text = []; + this.opentag = []; + this.closetag = []; + this.error = []; + this.complete = []; + this.end = []; - var StatusHandler = { - tag: 'DAV:/status', + var events = [ + 'ontext', 'onclosetag', + 'onopentag', 'onerror', + 'oncomplete', 'onend' + ]; + } - onopentag: function(data) { - }, + TestHander.prototype = { - ontext: function(data) { - this.current.status = data.match(/([0-9]{3,3})/)[1]; - }, + ontext: function(data, handler) { + handler.text.push(data); + }, - onclosetag: function(data) { - this.restoreParser(); - } - }; + onclosetag: function(data, handler) { + handler.closetag.push(data); + }, - var ResourceTypeHandler = { - tag: 'DAV:/resourcetype', + onopentag: function(data, handler) { + handler.opentag.push(data); + }, - onopentag: function(data) { - this.tagStack.push(data.tagSpec); + onerror: function(data, handler) { + handler.error.push(data); + }, - if (data.local === 'resourcetype') { - this.current.resourceTypes = []; - } else { - this.current.resourceTypes.push(data.local); - } - }, + oncomplete: function(data, handler) { + handler.complete.push(data); + }, - onclosetag: function(data) { - this.checkStackForHandler(true); - this.tagStack.pop(); - } - }; + onend: function(data) { + handler.end.push(data); + } + }; - var TextOnlyHandler = { - tag: 'DAV:/href', + function firesHandler(type, data) { + var len = handler[type].length; + var event = handler[type][len - 1]; - onopentag: function(data) { - }, + assert.deepEqual(event, data); + } - ontext: function(data) { - this.current.href = data; - }, + suiteSetup(function() { + SAX = Webcals.require('sax'); + Base = Webcals.require('sax/base'); + }); - onclosetag: function(data) { - this.restoreParser(); - } - }; + setup(function() { + handler = new TestHander(); + subject = new SAX(handler); + }); + + test('initializer', function() { + assert.equal(subject.handler, handler); + assert.deepEqual(subject.stack, []); + assert.deepEqual(subject.handles, {}); + assert.deepEqual(subject._handlerStack, []); + assert.deepEqual(subject.tagStack, []); + assert.ok(subject._parse); + }); + + suite('#setHandler', function() { + + setup(function() { + subject.setHandler(handler, false); + }); - var ResponseHandler = { - tag: 'DAV:/response', - handles: { - 'DAV:/status': StatusHandler, - 'DAV:/resourcetype': ResourceTypeHandler, - 'DAV:/href': TextOnlyHandler, - 'DAV:/getetag': TextOnlyHandler - }, + test('set without store', function() { + assert.equal(subject.handler, handler); + + assert.equal( + subject._handlerStack.length, + 0, + 'should not save original' + ); + }); + + test('set/store', function() { + var uniq = {}; + + subject.setHandler(uniq, true); + + assert.equal(subject.handler, uniq); + assert.equal(subject._handlerStack[0], handler); + }); + + }); + + test('#restoreHandler', function() { + var uniq = {}; + subject.setHandler(uniq, true); + subject.restoreHandler(); + + assert.equal(subject.handler, handler); + }); - onclosetag: function(data) { - this.checkStackForHandler(true); - this.onclosetag(data); - }, + test('#registerHandler', function() { + var uniq = {}; - oncomplete: function() { - this.emit('response', this.current, this); - } + subject.registerHandler('a/foo', uniq); + assert.equal(subject.handles['a/foo'], uniq); + }); + test('#write', function() { + var called, uniq = {}; + subject._parse.write = function() { + return uniq; }; - parser.addHandler(ResponseHandler); + assert.equal(subject.write(), uniq); + }); + + test('#closed', function() { + assert.isFalse(subject.closed, 'should not be closed'); + + subject._parse.closed = true; + assert.isTrue( + subject.closed, + 'should be closed now that parser is.' + ); + }); + + suite('#getHandler', function() { + test('handler not found', function() { + assert.isFalse(subject.getHandler('foo')); + }); + + test('handler found', function() { + var uniq; + + subject.registerHandler('foo', uniq); + + var handler = subject.getHandler('foo'); + assert.equal(uniq, handler); + }); + + test('handler found but is current', function() { + var uniq = {}; + subject.registerHandler('foo', uniq); + subject.setHandler(uniq); + + assert.isFalse(subject.getHandler('foo')); + }); + }); + + suite('#onopentag', function() { + + test('basic event', function() { + var obj = { + local: 'foo', + uri: 'bar' + }; + + subject.onopentag(obj); + assert.equal(subject.currentTag, obj); + assert.equal(obj.tagSpec, 'bar/foo'); + assert.deepEqual( + subject.tagStack, + [{ tag: 'bar/foo' }] + ); + + firesHandler('opentag', obj); + }); + }); + + suite('handler stacks', function() { + var newHandler; - parser.on('response', function(data, context) { - console.log(JSON.stringify(data), '\n\n'); + setup(function() { + newHandler = new TestHander(); + subject.registerHandler('a/a', newHandler); + subject.onopentag({ + local: 'a', + uri: 'a' + }); }); - parser.once('complete', function(data, parser) { - console.log(JSON.stringify(data)); + test('switch to new handler', function() { + assert.equal(subject.handler, newHandler); + assert.deepEqual( + subject.tagStack, [ + { tag: 'a/a', handler: newHandler } + ] + ); + }); + + test('pop to original handler', function() { + subject.onclosetag('a/a'); + assert.equal(subject.tagStack.length, 0, 'should clear tagStack'); + assert.equal(subject.handler, handler, 'should reset handler'); + assert.equal( + newHandler.complete.length, 1, + 'should fire complete event on new handler' + ); + }); + + }); + + test('#onclosetag', function() { + var obj = { local: 'a', uri: 'b' }; + + subject.onopentag(obj); + assert.equal(subject.tagStack.length, 1); + + subject.onclosetag('a:b'); + assert.equal(subject.tagStack.length, 0); + + firesHandler('closetag', 'a:b'); + }); + + test('#ontext', function() { + subject.ontext('foo'); + firesHandler('text', 'foo'); + }); + + test('#onerror', function() { + subject.onerror('foo'); + firesHandler('error', 'foo'); + }); + + test('#onend', function() { + subject.onend(); + assert.ok(handler.end); + assert.equal(handler.end[0], subject.root); + }); + + suite('complex mutli-handler', function() { + var xml, + expected, + events; + + var ResponseHandler; + var TextOnlyHandler; + + defineSample('xml/complex-tree.xml', function(data) { + xml = data; + }); + + suiteSetup(function() { + TextOnlyHandler = Base.create({ + name: 'text', + + //don't add text only elements + //to the stack as objects + onopentag: function() {}, + onclosetag: function() {}, + + //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]] = data; + } + }); + + ResponseHandler = Base.create({ + name: 'response', + + handles: { + 'DAV:/href': TextOnlyHandler, + 'DAV:/status': TextOnlyHandler, + 'DAV:/getetag': TextOnlyHandler + }, + + oncomplete: function() { + events.push(this.current); + } + }); + }); + + setup(function() { + //use real handlers + subject.setHandler(Base); + }); + + test('complex result', function() { + var result, + expectedEvent, + expectedResult; + + events = []; + + expectedEvent = { + href: 'uri', + propstat: [ + { + status: '400', + prop: { + current: {} + } + }, + { + status: '200', + prop: { + next: {} + } + } + ] + }; + + expectedResult = { + complex: { + response: [{}, expectedEvent] + } + }; + + subject.registerHandler('DAV:/response', ResponseHandler); + + subject.on('response', function(data) { + events.push(data); + }); + + subject.once('complete', function(data) { + result = data; + }); + + subject.write(xml).close(); + + assert.ok(events[0]); + assert.equal(events.length, 2); + + assert.ok(result); + + assert.deepEqual(events[0], {}); + assert.deepEqual(events[1], expectedEvent); + assert.deepEqual(result, expectedResult); }); - parser.write(data).close(); }); }); |