jaxon
Version:
Jaxon is a sequential access, event-driven JSON parser developed specifically to deal with streams.
1,261 lines (1,124 loc) • 36.5 kB
JavaScript
/**
* Copyright (c) 2013, Dan Eyles (dan@irlgaming.com)
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name of IRL Gaming nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL IRL Gaming BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
"use strict";
var fs = require('fs');
var http = require('http');
var https = require('https');
var url = require('url');
module.exports = {
classes: {},
objects: {},
constants: {
// delimiting tokens
__OBJ_OPEN:'{',
__OBJ_CLOSE:'}',
__OBJ_DELIM:':',
__ARR_OPEN:'[',
__ARR_CLOSE:']',
__STR_DELIM:'"',
__ELEM_DELIM:',',
__STR_ESCAPE:'\\',
__CHAR_NEWLINE:'\n',
__CHAR_SPACE:' ',
__CHAR_TAB:'\t',
// types
__TYPE_OBJECT: 0,
__TYPE_ARRAY: 1,
__TYPE_STRING: 2,
__TYPE_NUMBER: 4,
__TYPE_BOOLEAN: 5,
__TYPE_NULL: 6,
__TYPE_HEX: 7
}
};
var __c = module.exports.constants;
/**
* A simple stack to support JSON parsing
* @class {Stack}
* @return {Void}
*/
module.exports.classes.Stack = function() {
/**
* @type Array
*/
this.stack = [];
/**
* Returns the element at the top of the stack without modify the data structure
* @return {Mixed|null}
*/
this.peek = function() {
if (this.length() == 0) {
return null;
}
return this.stack[this.stack.length - 1];
};
/**
* Pushes an element on to the stack
* @param {Mixed} element the element to push on to the stack
* @return {Void}
*/
this.push = function(element) {
this.stack.push(element);
};
/**
* Pops an element off the stack, returning NULL if the stack is empty
* @return {Mixed|null}
*/
this.pop = function() {
if (this.length() == 0) {
return null;
}
return this.stack.pop();
};
/**
* Returns the number of elements in the stack as an integer
* @return {Integer}
*/
this.length = function() {
return this.stack.length;
};
this.empty = function() {
return this.length() == 0;
};
this.clear = function() {
this.stack = [];
};
this.toString = function() {
return this.stack.join(' -> ');
};
};
/**
* Represents a simple hash map
* @class {HashMap}
* @return {Void}
*/
module.exports.classes.HashMap = function() {
/**
* @type {Integer}
*/
this.size = 0;
/**
* @type {Object}
*/
this.table = {};
/**
* Adds a key/value pair to the table, overwriting previous entries
* @param {String} key
* @param {Mixed} value
* @return {Boolean}
*/
this.set = function(key, value) {
if (!this.contains(key)) {
this.size++;
}
this.table[key] = value;
return true;
};
/**
* Removes the entry from the table that corresponds to the provided key.
* If none exists returns false - otherwise returns true.
* @param {String} key the key to remove
* @return {Boolean}
*/
this.remove = function(key) {
if (this.contains(key)) {
delete this.table[key];
this.size--;
return true;
}
return false;
};
/**
* Returns a boolean value indicating whether or not the provided key
* corresponds to an entry in the table
* @param {String} key the key to query for
* @return {Boolean}
*/
this.contains = function(key) {
return this.table.hasOwnProperty(key);
};
/**
* Returns the entry corresponding to the provided key, returning null
* if none exists.
* @param {String} key the key to query for
* @return {Mixed|null}
*/
this.get = function(key) {
if (!this.contains(key)) {
return null;
}
return this.table[key];
};
/**
* Returns the number of entries in the table as an integer
* @return {Integer}
*/
this.length = function() {
return this.size;
};
};
/**
* Represents an event in the observer pattern
* @class {ObservableEvent}
* @param {String} name the name of the event
* @param {Mixed} body the body of the event
* @return {Void}
*/
module.exports.classes.ObservableEvent = function(name, body) {
/**
* @type {String}
*/
this.name = name;
/**
* @type {Mixed}
*/
this.body = body;
/**
* Returns the name of the event
* @return {String}
*/
this.getName = function() {
return this.name;
};
/**
* Returns the body of the event
* @return {Mixed}
*/
this.getBody = function() {
return this.body;
};
};
/**
* Represents an observer implementation in the observer pattern
* @class Observer
* @param {Function} callback the callback to notify of events
* @return {Void}
*/
module.exports.classes.Observer = function(callback) {
/**
* @type {Function}
*/
this.callback = callback;
/**
* Notifies the observer callback of an event
* @param {ObservableEvent} event the event to notify the observer callback with
* @return {Void}
*/
this.notify = function(event) {
this.callback(event);
};
};
/**
* Represents an observable object in the observer pattern
* @return {Void}
*/
module.exports.classes.Observable = function() {
/**
* @type {Object}
*/
this.listeners = {};
/**
* Registers an observer for a given event name
* @param {String} name the name of the event to observe
* @param {Observer} observer the observer to register
* @return {Void}
*/
this.registerObserver = function(name, observer) {
if (!this.listeners.hasOwnProperty(name)) {
this.listeners[name] = [];
}
this.listeners[name].push(observer);
}
/**
* Un-registers an observer for a given event name
* @param {String} name the name of the event to observe
* @param {Observer} observer the observer to remove
* @return {Boolean}
*/
this.unregisterObserver = function(name, observer) {
if (!this.listeners.hasOwnProperty(name)) {
return false;
}
var listeners = [];
for (var key in this.listeners) {
if (!this.listeners.hasOwnProperty(key)) {
continue;
}
this.listeners[key].forEach(function(listener) {
if (observer !== listener) {
listeners.push(listener);
}
});
}
this.listeners[name] = listeners;
return true;
};
/**
* Notifies all observers of an event
* @param {ObservableEvent} event the event to propogate to observers
* @return {Boolean}
*/
this.notifyObservers = function(event) {
if (!this.listeners.hasOwnProperty(event.getName())) {
return false;
}
this.listeners[event.getName()].forEach(function(listener) {
listener.notify(event);
});
return true;
};
/**
* Registers an event listener on the stream
* @param {String} name the name of the event to observe
* @param {Function} callback the callback to execute when observing the event
* @return {Boolean}
*/
this.on = function(name, callback) {
if (!callback) {
throw new Error('A callback function that accepts one parameter is required');
}
if (Object.prototype.toString.call(callback) !== '[object Function]') {
throw new Error('Callback must be a function');
}
var observer = new module.exports.classes.Observer(callback);
return this.registerObserver(name, observer);
};
};
/**
* Represents a character stream
* @class {Stream}
* @extends {Observable}
* @return {Void}
*/
module.exports.classes.Stream = function() {
module.exports.classes.Observable.call(this);
/**
* @type {Array}
*/
this.buffer = [];
/**
* Returns the number of characters currently buffered in the stream as an integer
* @return {Integer}
*/
this.length = function() {
return this.buffer.length;
};
/**
* Returns a string representation of the contents of the stream
* @param {Function} callback the callback to execute when concatenting the stream contents to a string
* @return {String}
*/
this.toString = function(callback) {
if (!callback) {
return this.buffer.join('');
}
if (this.buffer.length == 0) {
var error = new Error('stream is empty');
if (!callback) {
throw error;
}
callback(error, null);
return;
}
if (!callback) {
return this.buffer.join('');
}
callback(null, this.buffer.join(''));
};
/**
* Writes a single character to the stream
* @param {Char} data
* @return {Void}
*/
this.writeChar = function(data) {
this.buffer.push(data);
this.notifyObservers(new module.exports.classes.ObservableEvent('write', data));
};
/**
* Writes the contents of a string to the stream
* @param {String} string
* @return {Void}
*/
this.writeString = function(string) {
var data = string.split('');
this.buffer = this.buffer.concat(data);
this.notifyObservers(new module.exports.classes.ObservableEvent('write', data));
};
/**
* Writes the contents of the array to the stream
* @param {Array} data
* @return {Void}
*/
this.writeArray = function(data) {
this.buffer = this.buffer.concat(data);
this.notifyObservers(new module.exports.classes.ObservableEvent('write', data));
};
/**
* Reads a single character from the stream
* @param {Function} callback the callback to execute when reading from the stream
* @return {Void|Char}
*/
this.read = function(callback) {
if (!callback) {
return this.buffer.shift();
}
if (this.buffer.length == 0) {
var error = new Error('stream is empty');
if (!callback) {
throw error;
}
callback(error, null);
return false;
}
var data = this.buffer.shift();
if (!callback) {
return data;
}
callback(null, data);
return true;
};
/**
* Flushes all data from the stream
* @param {Function} callback the callback to execute when flushing the stream
* @return {Void|Array}
*/
this.flush = function(callback) {
var data = null;
if (this.buffer.length == 0) {
var error = new Error('stream is empty');
if (!callback) {
throw error;
}
callback(error, null);
return;
}
data = this.buffer.splice(0);
if (!callback) {
return data;
}
callback(null, data);
this.notifyObservers(new module.exports.classes.ObservableEvent('flush', data));
};
};
/**
* Represents a "chunk" of characters currently being handled
* @param {Integer} type the type code for the chunk (e.g. number, string, boolean, null)
* @class {Chunk}
* @return {Void}
*/
module.exports.classes.Chunk = function(type) {
this.buffer = [];
this.type = type;
/**
* Appends a character to the chunk
* @param {Char|Integer} token
* @return {Void}
*/
this.append = function(token) {
this.buffer.push(token);
};
/**
* Returns the value for the chunk, this can vary depending on the type
* code assigned to the chunk
* @throws {Error}
* @return {Mixed}
*/
this.getValue = function() {
switch (this.type) {
case __c.__TYPE_BOOLEAN:
return this.getBooleanValue();
case __c.__TYPE_NUMBER:
return this.getNumericValue();
case __c.__TYPE_NULL:
return this.getNullValue();
}
return this.buffer.join('');
};
this.getNullValue = function() {
var value = this.buffer.join('').toLowerCase();
if (value !== 'null') {
throw new Error('invalid token: ' + value);
}
return null;
};
this.getBooleanValue = function() {
var value = this.buffer.join('').toLowerCase();
switch (value) {
case 'true':
return true;
break;
case 'false':
return false;
break;
}
throw new Error('invalid boolean value: ' + value);
};
this.getNumericValue = function() {
var value = this.buffer.join('');
if (value.match(/[^\deE\+\-\.]/g)) {
throw new Error('invalid numeric value: ' + value);
}
if (!value.match(/[eE\.]/)) {
return parseInt(value);
} else {
return parseFloat(value);
}
};
/**
* Returns the type code for the chunk
* @return {Integer}
*/
this.getType = function() {
return this.type;
};
/**
* Returns a string representation of the chunk
* @return {String}
*/
this.toString = function() {
return this.buffer.join('');
};
};
/**
* A simple class containing logic for parsing JSON stream tokens
* @param {StreamParser} the stream parser to handle tokens for
* @class {TokenHandler}
* @return {Void}
*/
module.exports.classes.TokenHandler = function(parser) {
module.exports.classes.Observable.call(this);
/**
* @type {StreamParser}
*/
this.parser = parser;
/**
* @type {Stack}
*/
this.chunks = new module.exports.classes.Stack();
/**
* @type {Stack}
*/
this.keys = new module.exports.classes.Stack();
this.objKeys = new module.exports.classes.Stack();
/**
* a map containing characters to ignore outside of string values
* @type {Object}
*/
this.ignore = {};
this.ignore[__c.__CHAR_NEWLINE] = true;
this.ignore[__c.__CHAR_SPACE] = true;
this.ignore[__c.__CHAR_TAB] = true;
/**
* a map of all unicode control characters with shorthand encoding. The map
* is keyed by character and contains the unicode sequence used to encode it.
* @type {Object}
*/
this.control = {};
this.control['b'] = '0008'; // backspace
this.control['t'] = '0009'; // tab
this.control['n'] = '000A'; // newline
this.control['r'] = '000D'; // carriage return
this.control['f'] = '000C'; // form feed
var scope = this;
/**
* the internal state of the parser
* @type {Object}
*/
this.state = {
escaped: false, // whether or not the current string is escaped
string: false, // whether or not we're currently inside a string
unicode: false, // whether or not we're collecting tokens for a unicode value
unibuff: [] // the character buffer for unicode values
};
/**
* a map of handler closures, keyed by the token they handle
* @type {Object}
*/
this.handlers = {};
/**
* Registers a handler function corresponding to a given character token
* @param {Char} token
* @param {Function} callback
* @return {Void}
*/
this.registerHandler = function(token, callback) {
callback = callback.bind(scope);
this.handlers[token] = callback;
};
this.determineType = function(token) {
if (!token) {
return false;
}
if (token.match(/[0-9\.\-]/)) {
return __c.__TYPE_NUMBER;
} else if(token.match(/[tTfF]/)) {
return __c.__TYPE_BOOLEAN;
} else if(token.match(/[nN]/)) {
return __c.__TYPE_NULL;
}
throw new Error('unable to determine type for token ' + token);
};
this.determineClass = function(o) {
if (!o) {
return false;
}
if (Object.prototype.toString.call(o) === '[object Array]') {
return __c.__TYPE_ARRAY;
}
if (Object.prototype.toString.call(o) === '[object Object]') {
return __c.__TYPE_OBJECT;
}
return __c.__TYPE_STRING;
};
this.addNewElement = function(o, scalar) {
var element = this.parser.elements.peek();
var type = this.determineClass(element);
switch (type) {
case __c.__TYPE_ARRAY:
element.push(o);
break;
case __c.__TYPE_OBJECT:
if (this.keys.empty()) {
throw new Error('unable to add value to object with key');
}
var key = this.keys.pop();
element[key] = o;
this.notifyObservers(new module.exports.classes.ObservableEvent('found', {
'key': key,
'value': o
}));
this.objKeys.push(key);
break;
}
if (!scalar) {
this.parser.elements.push(o);
}
};
this.addNewChunk = function() {
if (this.chunks.empty()) {
return false;
}
var chunk = this.chunks.peek();
var element = this.parser.elements.peek();
var type = this.determineClass(element);
switch (type) {
case __c.__TYPE_ARRAY:
element.push(chunk.getValue());
break;
case __c.__TYPE_OBJECT:
if (this.keys.empty()) {
var key = chunk.getValue();
this.keys.push(key);
this.notifyObservers(new module.exports.classes.ObservableEvent('found', {
'key': key
}));
} else {
var key = this.keys.pop();
var value = chunk.getValue();
element[key] = chunk.getValue();
this.notifyObservers(new module.exports.classes.ObservableEvent('parsed', {
'key': key,
'value': value
}));
}
break;
default:
throw new Error('badly formed JSON stream');
break;
}
this.state.string = false;
this.state.escaped = false;
this.chunks.pop();
return true;
};
this.registerHandler('__default__', function(token) {
if (this.state.string) {
if (this.state.escaped) {
this.state.escaped = !this.state.escaped;
if (this.control.hasOwnProperty(token)) {
return this.chunks.peek().append(String.fromCharCode(parseInt(this.control[token], 16)));
} else if (token === 'u') {
this.state.unicode = true;
return;
}
}
if (this.state.unicode) {
this.state.unibuff.push(token);
if (token.match('/[^0-9a-fA-F]/')) {
throw new Error("invalid unicode sequence " + this.state.unibuff.join(''));
}
if (this.state.unibuff.length == 4) {
var unicode = this.state.unibuff.join('');
if (unicode === '0022') {
throw new Error('invalid unicode sequence \u0022');
}
this.chunks.peek().append(String.fromCharCode(parseInt(unicode, 16)));
this.state.unicode = false;
this.state.unibuff = [];
}
return;
} else {
if (this.chunks.peek().getType() !== __c.__TYPE_STRING
&& this.ignore.hasOwnProperty(token)) {
return;
}
return this.chunks.peek().append(token);
}
}
if (this.ignore.hasOwnProperty(token)) {
return false;
}
var chunk = new module.exports.classes.Chunk(this.determineType(token));
chunk.append(token);
chunk.open = true;
this.state.string = true;
this.state.escaped = false;
this.chunks.push(chunk);
});
this.registerHandler(__c.__STR_ESCAPE, function(token) {
if (this.state.string) {
if (this.state.escaped) {
this.chunks.peek().append(token);
}
this.state.escaped = !this.state.escaped;
} else {
throw new Error('unable to escape value outside string');
}
});
this.registerHandler(__c.__OBJ_OPEN, function(token) {
if (this.state.string) {
return this.chunks.peek().append(token);
}
this.addNewElement({});
});
this.registerHandler(__c.__OBJ_CLOSE, function(token) {
if (this.state.string && this.chunks.peek().getType() == __c.__TYPE_STRING) {
return this.chunks.peek().append(token);
}
this.addNewChunk();
var key = this.objKeys.pop();
var element = this.parser.elements.peek();
var type = this.determineClass(element);
if (type !== __c.__TYPE_OBJECT) {
throw new Error('badly formed object');
}
element = this.parser.elements.pop();
this.notifyObservers(new module.exports.classes.ObservableEvent('parsed', {
'key': key,
'value': element
}));
});
this.registerHandler(__c.__ARR_OPEN, function(token) {
if (this.state.string) {
return this.chunks.peek().append(token);
}
this.addNewElement([]);
});
this.registerHandler(__c.__ARR_CLOSE, function(token) {
if (this.state.string && this.chunks.peek().getType() == __c.__TYPE_STRING) {
return this.chunks.peek().append(token);
}
this.addNewChunk();
var element = this.parser.elements.peek();
var type = this.determineClass(element);
if (type !== __c.__TYPE_ARRAY) {
throw new Error('badly formed array');
}
element = this.parser.elements.pop();
if (!this.objKeys.empty()
&& this.determineClass(this.parser.elements.peek()) == __c.__TYPE_OBJECT) {
var key = this.objKeys.pop();
this.notifyObservers(new module.exports.classes.ObservableEvent('parsed', {
'key': key,
'value': element
}));
}
});
this.registerHandler(__c.__OBJ_DELIM, function(token) {
var chunk = this.chunks.peek();
if (this.state.string) {
return chunk.append(token);
}
this.addNewChunk();
});
this.registerHandler(__c.__ELEM_DELIM, function(token) {
if (this.state.string && this.chunks.peek().getType() == __c.__TYPE_STRING) {
return this.chunks.peek().append(token);
}
return this.addNewChunk();
});
this.registerHandler(__c.__STR_DELIM, function(token) {
var chunk = null;
if (this.state.string) {
chunk = this.chunks.peek();
if (this.state.escaped) {
return chunk.append(token);
}
chunk.open = false;
this.state.string = false;
this.state.escaped = false;
return;
}
chunk = new module.exports.classes.Chunk(__c.__TYPE_STRING);
chunk.open = true;
this.state.string = true;
this.state.escaped = false;
this.chunks.push(chunk);
});
/**
* Executes the registered handler for the provided token
* @param {Char} the token to handle
* @return {Void}
*/
this.handle = function(token) {
if (!this.handlers.hasOwnProperty(token)) {
return this.handlers['__default__'](token);
}
return this.handlers[token](token);
};
};
/**
* Represents a parser for JSON character streams
* @class {StreamParser}
* @return {Void}
*/
module.exports.classes.StreamParser = function() {
module.exports.classes.Observable.call(this);
/**
* @type {Function}
*/
var __t = this;
/**
* @type {Stream}
*/
this.stream = new module.exports.classes.Stream();
/**
* @type {Stack}
*/
this.elements = new module.exports.classes.Stack();
/**
* @type {TokenHandler}
*/
this.handler = new module.exports.classes.TokenHandler(this);
/**
* internal event handlers below
*/
this.handler.on('found', function(data) {
var body = data.getBody();
if (!body.key) {
return;
}
__t.notifyObservers(new module.exports.classes.ObservableEvent('found:' + body.key, body));
});
this.handler.on('parsed', function(data) {
var body = data.getBody();
if (!body.key) {
return;
}
__t.notifyObservers(new module.exports.classes.ObservableEvent('parsed:' + body.key, body.value));
});
this.handler.on('parsed', function(data) {
__t.notifyObservers(data);
});
/**
* end internal event handlers
*/
/**
* Fires an event to all subscribed observers when a given key
* is encountered in the stream. This event is fired when the key is first
* seen, but the body of the corresponding value has not been parsed.
* @param {String} key the key to watch for
* @param {Function} callback the function to execute when the key is encountered
* @return {Void}
*/
this.onFind = function(key, callback) {
this.on('found:' + key, function(event) {
callback(null, event.getBody());
});
};
/**
* Fires an event to all subscribed observers when the corresponding value for
* a given key is parsed. This event is fired when the value is completely parsed.
* @param {String} key the key to watch for
* @param {Function} callback the function to execute when the value
* corresponding to the key is parsed
* @return {Void}
*/
this.onParse = function(key, callback) {
this.on('parsed:' + key, function(event) {
callback(null, event.getBody());
});
};
/**
* The same as onParse, however the key is matched against the provided pattern
* rather than exactly matched. This function allows to fuzzy matching against
* key names (e.g. /^(foo|bar)$/, /key\d{1,3}/)
* @param {RegExp} pattern the pattern to match keys against
* @param {Function} callback the function to execute when the value
* corresponding to the key is parsed
* @return {Void}
*/
this.onMatch = function(pattern, callback) {
this.on('parsed', function(event) {
var body = event.getBody();
if (!body.key) {
return;
}
if (body.key.match(pattern)) {
callback(null, event.getBody());
}
});
};
/**
* Fires an event when an error occurs during parsing, the thrown error is
* provided as the sole argument for the function.
* @param {Function} callback the function to execute when an error occurs
* @return {Void}
*/
this.onError = function(callback) {
this.on('error', function(event) {
callback(event.getBody(), null);
});
};
/**
* Resets the scope for the parser - effectively discardingly previously
* parsed elements and starting from scratch
* @return {Void}
*/
this.resetScope = function() {
this.elements.clear();
this.elements.push([]);
this.scope = this.elements.peek();
};
/**
* Consumes the provided source string and writes to the parser character stream
* @param {String} source
* @return {Void}
*/
this.consume = function(source) {
this.stream.writeString(source);
};
/**
* Parses data as it is written to the character stream
* @param {Mixed} data
* @return {Void}
*/
this.parse = function(data) {
try {
while(__t.stream.read(function(err, token) {
if (err) {
return;
}
__t.handler.handle(token);
}));
} catch (e) {
__t.resetScope();
__t.notifyObservers(new module.exports.classes.ObservableEvent('error', e));
}
};
/**
* Returns the scope for the parser. The scope represents the top level array container
* for the stream being parsed. Holding a pointer to this scope allows agents to access
* the JSON encoded objects in real time as they are being parsed.
* @return {Array}
*/
this.getScope = function() {
return this.scope;
};
this.resetScope();
this.stream.on('write', this.parse);
};
/**
* An implementation of the facade pattern that ties all JAXON parser logic together
* in a neat, simple API.
* @class {Jaxon}
* @return {Void}
*/
module.exports.classes.Jaxon = function() {
this.parser = new module.exports.classes.StreamParser();
this.completeCallback = null;
var scope = this;
this.parse = function(path, options, callback) {
options = options || {};
if (path.match(/^file\:\/\//)) {
this.useFileStream(path, options, callback);
} else if (path.match(/^(http|https)\:\/\//)) {
this.useHttpStream(path, options, callback);
} else {
callback(new Error('unrecognised url scheme: ' + path));
}
return this.parser.getScope();
};
this.use = function(stream, callback) {
stream.on('error', function(e) { callback(e); });
stream.on('data', function(buffer) {
scope.parser.consume(buffer.toString());
});
stream.on('end', function() {
if (scope.completeCallback) {
scope.completeCallback(scope.parser.getScope());
}
scope.parser.resetScope();
});
return this.parser.getScope();
};
this.on = function(event, key, callback) {
switch (event) {
case 'error':
callback = key;
this.parser.onError(callback);
break;
case 'complete':
callback = key;
if (Object.prototype.toString.call(callback) !== '[object Function]') {
throw new Error('callback must be a function');
}
this.completeCallback = callback;
break;
case 'parse':
this.parser.onParse(key, callback);
break;
case 'match':
this.parser.onMatch(key, callback);
break;
case 'find':
this.parser.onFind(key, callback);
break;
default:
callback(new Error('unrecognized event: ' + event));
break;
}
return this;
};
this.useFileStream = function(path, options, callback) {
var p = path.replace(/^file\:\/\//, '');
var stream = fs.createReadStream(p, {
'flags':'r',
'encoding':'utf8',
'bufferSize': 4100
});
this.use(stream);
};
this.useHttpStream = function(path, options, callback) {
var opt = url.parse(path);
if (options['user-agent']) {
opt['user-agent'] = options['user-agent'];
}
if (options['accept']) {
opt['accept'] = options['accept'];
}
var handler = null;
switch (opt['protocol']) {
case 'http:':
handler = http;
break;
case 'https:':
handler = https;
break;
default:
callback(new Error('unrecognized protocol: ' + opt['protocol']));
break;
}
handler.get(opt, function(res) {
scope.use(res);
});
};
};
/**
* Returns a new Stack instance
* @return {Stack}
*/
module.exports.factoryStack = function() {
return new module.exports.classes.Stack();
};
/**
* Returns a new HashMap instance
* @return {HashMap}
*/
module.exports.factoryHashMap = function() {
return new module.exports.classes.HashMap();
};
/**
* Returns a new Stream instance
* @return {Stream}
*/
module.exports.factoryStream = function() {
return new module.exports.classes.Stream();
};
/**
* Returns a new ObservableEvent instance
* @return {ObservableEvent}
*/
module.exports.factoryObservableEvent = function(name, body) {
return new module.exports.classes.ObservableEvent(name, body);
};
/**
* Returns a new Observable instance
* @return {Observable}
*/
module.exports.factoryObservable = function() {
return new module.exports.classes.Observable;
};
/**
* Returns a new Observer instance
* @return {Observer}
*/
module.exports.factoryObserver = function(callback) {
return new module.exports.classes.Observer(callback);
};
/**
* Returns a new Chunk instance
* @param {Integer} type the type code for the chunk
* @return {Chunk}
*/
module.exports.factoryChunk = function(type) {
return new module.exports.classes.Chunk(type);
};
/**
* Returns a new StreamParser instance
* @return {StreamParser}
*/
module.exports.factoryStreamParser = function(scope) {
return new module.exports.classes.StreamParser(scope);
}
/**
* Returns a new TokenHandler instance
* @param {StreamParser} parser
* @return {TokenHandler}
*/
module.exports.factoryTokenHandler = function(parser) {
return new module.exports.classes.TokenHandler(parser);
};
/**
* Returns a new Jaxon facade instance
* @param {String} path
* @param {Function} callback
* @return {Jaxon}
*/
module.exports.factory = function() {
return new module.exports.classes.Jaxon();
};