// This file is the concatenation of many js files. | |
// See https://github.com/jimhigson/oboe.js for the raw source | |
(function (window, Object, Array, Error, JSON, undefined ) { | |
// v1.12.3-1-g3e82471 | |
/* | |
Copyright (c) 2013, Jim Higson | |
All rights reserved. | |
Redistribution and use in source and binary forms, with or without | |
modification, are permitted provided that the following conditions are | |
met: | |
1. Redistributions of source code must retain the above copyright | |
notice, this list of conditions and the following disclaimer. | |
2. 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. | |
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 THE COPYRIGHT | |
HOLDER OR CONTRIBUTORS 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. | |
*/ | |
/** | |
* Partially complete a function. | |
* | |
* var add3 = partialComplete( function add(a,b){return a+b}, 3 ); | |
* | |
* add3(4) // gives 7 | |
* | |
* function wrap(left, right, cen){return left + " " + cen + " " + right;} | |
* | |
* var pirateGreeting = partialComplete( wrap , "I'm", ", a mighty pirate!" ); | |
* | |
* pirateGreeting("Guybrush Threepwood"); | |
* // gives "I'm Guybrush Threepwood, a mighty pirate!" | |
*/ | |
var partialComplete = varArgs(function( fn, args ) { | |
// this isn't the shortest way to write this but it does | |
// avoid creating a new array each time to pass to fn.apply, | |
// otherwise could just call boundArgs.concat(callArgs) | |
var numBoundArgs = args.length; | |
return varArgs(function( callArgs ) { | |
for (var i = 0; i < callArgs.length; i++) { | |
args[numBoundArgs + i] = callArgs[i]; | |
} | |
args.length = numBoundArgs + callArgs.length; | |
return fn.apply(this, args); | |
}); | |
}), | |
/** | |
* Compose zero or more functions: | |
* | |
* compose(f1, f2, f3)(x) = f1(f2(f3(x)))) | |
* | |
* The last (inner-most) function may take more than one parameter: | |
* | |
* compose(f1, f2, f3)(x,y) = f1(f2(f3(x,y)))) | |
*/ | |
compose = varArgs(function(fns) { | |
var fnsList = arrayAsList(fns); | |
function next(params, curFn) { | |
return [apply(params, curFn)]; | |
} | |
return varArgs(function(startParams){ | |
return foldR(next, startParams, fnsList)[0]; | |
}); | |
}); | |
/** | |
* A more optimised version of compose that takes exactly two functions | |
* @param f1 | |
* @param f2 | |
*/ | |
function compose2(f1, f2){ | |
return function(){ | |
return f1.call(this,f2.apply(this,arguments)); | |
} | |
} | |
/** | |
* Generic form for a function to get a property from an object | |
* | |
* var o = { | |
* foo:'bar' | |
* } | |
* | |
* var getFoo = attr('foo') | |
* | |
* fetFoo(o) // returns 'bar' | |
* | |
* @param {String} key the property name | |
*/ | |
function attr(key) { | |
return new Function('o', 'return o["' + key + '"]' ); | |
} | |
/** | |
* Call a list of functions with the same args until one returns a | |
* truthy result. Similar to the || operator. | |
* | |
* So: | |
* lazyUnion([f1,f2,f3 ... fn])( p1, p2 ... pn ) | |
* | |
* Is equivalent to: | |
* apply([p1, p2 ... pn], f1) || | |
* apply([p1, p2 ... pn], f2) || | |
* apply([p1, p2 ... pn], f3) ... apply(fn, [p1, p2 ... pn]) | |
* | |
* @returns the first return value that is given that is truthy. | |
*/ | |
var lazyUnion = varArgs(function(fns) { | |
return varArgs(function(params){ | |
var maybeValue; | |
for (var i = 0; i < len(fns); i++) { | |
maybeValue = apply(params, fns[i]); | |
if( maybeValue ) { | |
return maybeValue; | |
} | |
} | |
}); | |
}); | |
/** | |
* This file declares various pieces of functional programming. | |
* | |
* This isn't a general purpose functional library, to keep things small it | |
* has just the parts useful for Oboe.js. | |
*/ | |
/** | |
* Call a single function with the given arguments array. | |
* Basically, a functional-style version of the OO-style Function#apply for | |
* when we don't care about the context ('this') of the call. | |
* | |
* The order of arguments allows partial completion of the arguments array | |
*/ | |
function apply(args, fn) { | |
return fn.apply(undefined, args); | |
} | |
/** | |
* Define variable argument functions but cut out all that tedious messing about | |
* with the arguments object. Delivers the variable-length part of the arguments | |
* list as an array. | |
* | |
* Eg: | |
* | |
* var myFunction = varArgs( | |
* function( fixedArgument, otherFixedArgument, variableNumberOfArguments ){ | |
* console.log( variableNumberOfArguments ); | |
* } | |
* ) | |
* | |
* myFunction('a', 'b', 1, 2, 3); // logs [1,2,3] | |
* | |
* var myOtherFunction = varArgs(function( variableNumberOfArguments ){ | |
* console.log( variableNumberOfArguments ); | |
* }) | |
* | |
* myFunction(1, 2, 3); // logs [1,2,3] | |
* | |
*/ | |
function varArgs(fn){ | |
var numberOfFixedArguments = fn.length -1, | |
slice = Array.prototype.slice; | |
if( numberOfFixedArguments == 0 ) { | |
// an optimised case for when there are no fixed args: | |
return function(){ | |
return fn.call(this, slice.call(arguments)); | |
} | |
} else if( numberOfFixedArguments == 1 ) { | |
// an optimised case for when there are is one fixed args: | |
return function(){ | |
return fn.call(this, arguments[0], slice.call(arguments, 1)); | |
} | |
} | |
// general case | |
// we know how many arguments fn will always take. Create a | |
// fixed-size array to hold that many, to be re-used on | |
// every call to the returned function | |
var argsHolder = Array(fn.length); | |
return function(){ | |
for (var i = 0; i < numberOfFixedArguments; i++) { | |
argsHolder[i] = arguments[i]; | |
} | |
argsHolder[numberOfFixedArguments] = | |
slice.call(arguments, numberOfFixedArguments); | |
return fn.apply( this, argsHolder); | |
} | |
} | |
/** | |
* Swap the order of parameters to a binary function | |
* | |
* A bit like this flip: http://zvon.org/other/haskell/Outputprelude/flip_f.html | |
*/ | |
function flip(fn){ | |
return function(a, b){ | |
return fn(b,a); | |
} | |
} | |
/** | |
* Create a function which is the intersection of two other functions. | |
* | |
* Like the && operator, if the first is truthy, the second is never called, | |
* otherwise the return value from the second is returned. | |
*/ | |
function lazyIntersection(fn1, fn2) { | |
return function (param) { | |
return fn1(param) && fn2(param); | |
}; | |
} | |
/** | |
* A function which does nothing | |
*/ | |
function noop(){} | |
/** | |
* A function which is always happy | |
*/ | |
function always(){return true} | |
/** | |
* Create a function which always returns the same | |
* value | |
* | |
* var return3 = functor(3); | |
* | |
* return3() // gives 3 | |
* return3() // still gives 3 | |
* return3() // will always give 3 | |
*/ | |
function functor(val){ | |
return function(){ | |
return val; | |
} | |
} | |
/** | |
* This file defines some loosely associated syntactic sugar for | |
* Javascript programming | |
*/ | |
/** | |
* Returns true if the given candidate is of type T | |
*/ | |
function isOfType(T, maybeSomething){ | |
return maybeSomething && maybeSomething.constructor === T; | |
} | |
var len = attr('length'), | |
isString = partialComplete(isOfType, String); | |
/** | |
* I don't like saying this: | |
* | |
* foo !=== undefined | |
* | |
* because of the double-negative. I find this: | |
* | |
* defined(foo) | |
* | |
* easier to read. | |
*/ | |
function defined( value ) { | |
return value !== undefined; | |
} | |
/** | |
* Returns true if object o has a key named like every property in | |
* the properties array. Will give false if any are missing, or if o | |
* is not an object. | |
*/ | |
function hasAllProperties(fieldList, o) { | |
return (o instanceof Object) | |
&& | |
all(function (field) { | |
return (field in o); | |
}, fieldList); | |
} | |
/** | |
* Like cons in Lisp | |
*/ | |
function cons(x, xs) { | |
/* Internally lists are linked 2-element Javascript arrays. | |
Ideally the return here would be Object.freeze([x,xs]) | |
so that bugs related to mutation are found fast. | |
However, cons is right on the critical path for | |
performance and this slows oboe-mark down by | |
~25%. Under theoretical future JS engines that freeze more | |
efficiently (possibly even use immutability to | |
run faster) this should be considered for | |
restoration. | |
*/ | |
return [x,xs]; | |
} | |
/** | |
* The empty list | |
*/ | |
var emptyList = null, | |
/** | |
* Get the head of a list. | |
* | |
* Ie, head(cons(a,b)) = a | |
*/ | |
head = attr(0), | |
/** | |
* Get the tail of a list. | |
* | |
* Ie, head(cons(a,b)) = a | |
*/ | |
tail = attr(1); | |
/** | |
* Converts an array to a list | |
* | |
* asList([a,b,c]) | |
* | |
* is equivalent to: | |
* | |
* cons(a, cons(b, cons(c, emptyList))) | |
**/ | |
function arrayAsList(inputArray){ | |
return reverseList( | |
inputArray.reduce( | |
flip(cons), | |
emptyList | |
) | |
); | |
} | |
/** | |
* A varargs version of arrayAsList. Works a bit like list | |
* in LISP. | |
* | |
* list(a,b,c) | |
* | |
* is equivalent to: | |
* | |
* cons(a, cons(b, cons(c, emptyList))) | |
*/ | |
var list = varArgs(arrayAsList); | |
/** | |
* Convert a list back to a js native array | |
*/ | |
function listAsArray(list){ | |
return foldR( function(arraySoFar, listItem){ | |
arraySoFar.unshift(listItem); | |
return arraySoFar; | |
}, [], list ); | |
} | |
/** | |
* Map a function over a list | |
*/ | |
function map(fn, list) { | |
return list | |
? cons(fn(head(list)), map(fn,tail(list))) | |
: emptyList | |
; | |
} | |
/** | |
* foldR implementation. Reduce a list down to a single value. | |
* | |
* @pram {Function} fn (rightEval, curVal) -> result | |
*/ | |
function foldR(fn, startValue, list) { | |
return list | |
? fn(foldR(fn, startValue, tail(list)), head(list)) | |
: startValue | |
; | |
} | |
/** | |
* foldR implementation. Reduce a list down to a single value. | |
* | |
* @pram {Function} fn (rightEval, curVal) -> result | |
*/ | |
function foldR1(fn, list) { | |
return tail(list) | |
? fn(foldR1(fn, tail(list)), head(list)) | |
: head(list) | |
; | |
} | |
/** | |
* Return a list like the one given but with the first instance equal | |
* to item removed | |
*/ | |
function without(list, test, removedFn) { | |
return withoutInner(list, removedFn || noop); | |
function withoutInner(subList, removedFn) { | |
return subList | |
? ( test(head(subList)) | |
? (removedFn(head(subList)), tail(subList)) | |
: cons(head(subList), withoutInner(tail(subList), removedFn)) | |
) | |
: emptyList | |
; | |
} | |
} | |
/** | |
* Returns true if the given function holds for every item in | |
* the list, false otherwise | |
*/ | |
function all(fn, list) { | |
return !list || | |
( fn(head(list)) && all(fn, tail(list)) ); | |
} | |
/** | |
* Call every function in a list of functions with the same arguments | |
* | |
* This doesn't make any sense if we're doing pure functional because | |
* it doesn't return anything. Hence, this is only really useful if the | |
* functions being called have side-effects. | |
*/ | |
function applyEach(fnList, arguments) { | |
if( fnList ) { | |
head(fnList).apply(null, arguments); | |
applyEach(tail(fnList), arguments); | |
} | |
} | |
/** | |
* Reverse the order of a list | |
*/ | |
function reverseList(list){ | |
// js re-implementation of 3rd solution from: | |
// http://www.haskell.org/haskellwiki/99_questions/Solutions/5 | |
function reverseInner( list, reversedAlready ) { | |
if( !list ) { | |
return reversedAlready; | |
} | |
return reverseInner(tail(list), cons(head(list), reversedAlready)) | |
} | |
return reverseInner(list, emptyList); | |
} | |
function first(test, list) { | |
return list && | |
(test(head(list)) | |
? head(list) | |
: first(test,tail(list))); | |
} | |
/* | |
This is a slightly hacked-up browser only version of clarinet | |
with some features removed to help keep Oboe under | |
the 5k micro-library limit | |
For the original go here: | |
https://github.com/dscape/clarinet | |
*/ | |
var clarinet = (function () { | |
var clarinet = { | |
parser : function () { return new CParser();}, | |
CParser : CParser, | |
MAX_BUFFER_LENGTH : 64 * 1024, | |
EVENTS : [ | |
"value" | |
, "string" | |
, "key" | |
, "openobject" | |
, "closeobject" | |
, "openarray" | |
, "closearray" | |
, "error" | |
, "end" | |
, "ready" | |
] | |
}; | |
var buffers = [ "textNode", "numberNode" ] | |
, _n = 0 | |
; | |
var BEGIN = _n++; | |
var VALUE = _n++; // general stuff | |
var OPEN_OBJECT = _n++; // { | |
var CLOSE_OBJECT = _n++; // } | |
var OPEN_ARRAY = _n++; // [ | |
var CLOSE_ARRAY = _n++; // ] | |
var STRING = _n++; // "" | |
var OPEN_KEY = _n++; // , "a" | |
var CLOSE_KEY = _n++; // : | |
var TRUE = _n++; // r | |
var TRUE2 = _n++; // u | |
var TRUE3 = _n++; // e | |
var FALSE = _n++; // a | |
var FALSE2 = _n++; // l | |
var FALSE3 = _n++; // s | |
var FALSE4 = _n++; // e | |
var NULL = _n++; // u | |
var NULL2 = _n++; // l | |
var NULL3 = _n++; // l | |
var NUMBER_DECIMAL_POINT = _n++; // . | |
var NUMBER_DIGIT = _n; // [0-9] | |
if (!Object.create) { | |
Object.create = function (o) { | |
function f () { this["__proto__"] = o; } | |
f.prototype = o; | |
return new f; | |
}; | |
} | |
if (!Object.getPrototypeOf) { | |
Object.getPrototypeOf = function (o) { | |
return o["__proto__"]; | |
}; | |
} | |
if (!Object.keys) { | |
Object.keys = function (o) { | |
var a = []; | |
for (var i in o) if (o.hasOwnProperty(i)) a.push(i); | |
return a; | |
}; | |
} | |
function checkBufferLength (parser) { | |
var maxAllowed = Math.max(clarinet.MAX_BUFFER_LENGTH, 10) | |
, maxActual = 0 | |
; | |
for (var i = 0, l = buffers.length; i < l; i ++) { | |
var len = parser[buffers[i]].length; | |
if (len > maxAllowed) { | |
switch (buffers[i]) { | |
case "text": | |
closeText(parser); | |
break; | |
default: | |
error(parser, "Max buffer length exceeded: "+ buffers[i]); | |
} | |
} | |
maxActual = Math.max(maxActual, len); | |
} | |
parser.bufferCheckPosition = (clarinet.MAX_BUFFER_LENGTH - maxActual) | |
+ parser.position; | |
} | |
function clearBuffers (parser) { | |
for (var i = 0, l = buffers.length; i < l; i ++) { | |
parser[buffers[i]] = ""; | |
} | |
} | |
var stringTokenPattern = /[\\"\n]/g; | |
function CParser () { | |
var parser = this; | |
clearBuffers(parser); | |
parser.bufferCheckPosition = clarinet.MAX_BUFFER_LENGTH; | |
parser.q = parser.c = parser.p = ""; | |
parser.closed = parser.closedRoot = parser.sawRoot = false; | |
parser.tag = parser.error = null; | |
parser.state = BEGIN; | |
parser.stack = []; | |
// mostly just for error reporting | |
parser.position = parser.column = 0; | |
parser.line = 1; | |
parser.slashed = false; | |
parser.unicodeI = 0; | |
parser.unicodeS = null; | |
parser.depth = 0; | |
emit(parser, "onready"); | |
} | |
CParser.prototype = | |
{ end : function () { end(this); } | |
, write : write | |
, close : function () { return this.write(null); } | |
}; | |
function emit(parser, event, data) { | |
if (parser[event]) parser[event](data); | |
} | |
function emitNode(parser, event, data) { | |
closeValue(parser); | |
emit(parser, event, data); | |
} | |
function closeValue(parser, event) { | |
if (parser.textNode) { | |
emit(parser, (event ? event : "onvalue"), parser.textNode); | |
} | |
parser.textNode = ""; | |
} | |
function closeNumber(parser) { | |
if (parser.numberNode) | |
emit(parser, "onvalue", parseFloat(parser.numberNode)); | |
parser.numberNode = ""; | |
} | |
function error (parser, er) { | |
closeValue(parser); | |
er += "\nLine: "+parser.line+ | |
"\nColumn: "+parser.column+ | |
"\nChar: "+parser.c; | |
er = new Error(er); | |
parser.error = er; | |
emit(parser, "onerror", er); | |
return parser; | |
} | |
function end(parser) { | |
if (parser.state !== VALUE || parser.depth !== 0) | |
error(parser, "Unexpected end"); | |
closeValue(parser); | |
parser.c = ""; | |
parser.closed = true; | |
emit(parser, "onend"); | |
CParser.call(parser); | |
return parser; | |
} | |
function write (chunk) { | |
var parser = this; | |
// this used to throw the error but inside Oboe we will have already | |
// gotten the error when it was emitted. The important thing is to | |
// not continue with the parse. | |
if (this.error) | |
return; | |
if (parser.closed) return error(parser, | |
"Cannot write after close. Assign an onready handler."); | |
if (chunk === null) return end(parser); | |
var i = 0, c = chunk[0], p = parser.p; | |
while (c) { | |
p = c; | |
parser.c = c = chunk.charAt(i++); | |
// if chunk doesnt have next, like streaming char by char | |
// this way we need to check if previous is really previous | |
// if not we need to reset to what the parser says is the previous | |
// from buffer | |
if(p !== c ) parser.p = p; | |
else p = parser.p; | |
if(!c) break; | |
parser.position ++; | |
if (c === "\n") { | |
parser.line ++; | |
parser.column = 0; | |
} else parser.column ++; | |
switch (parser.state) { | |
case BEGIN: | |
if (c === "{") parser.state = OPEN_OBJECT; | |
else if (c === "[") parser.state = OPEN_ARRAY; | |
else if (c !== '\r' && c !== '\n' && c !== ' ' && c !== '\t') | |
error(parser, "Non-whitespace before {[."); | |
continue; | |
case OPEN_KEY: | |
case OPEN_OBJECT: | |
if (c === '\r' || c === '\n' || c === ' ' || c === '\t') continue; | |
if(parser.state === OPEN_KEY) parser.stack.push(CLOSE_KEY); | |
else { | |
if(c === '}') { | |
emit(parser, 'onopenobject'); | |
this.depth++; | |
emit(parser, 'oncloseobject'); | |
this.depth--; | |
parser.state = parser.stack.pop() || VALUE; | |
continue; | |
} else parser.stack.push(CLOSE_OBJECT); | |
} | |
if(c === '"') parser.state = STRING; | |
else error(parser, "Malformed object key should start with \""); | |
continue; | |
case CLOSE_KEY: | |
case CLOSE_OBJECT: | |
if (c === '\r' || c === '\n' || c === ' ' || c === '\t') continue; | |
if(c===':') { | |
if(parser.state === CLOSE_OBJECT) { | |
parser.stack.push(CLOSE_OBJECT); | |
closeValue(parser, 'onopenobject'); | |
this.depth++; | |
} else closeValue(parser, 'onkey'); | |
parser.state = VALUE; | |
} else if (c==='}') { | |
emitNode(parser, 'oncloseobject'); | |
this.depth--; | |
parser.state = parser.stack.pop() || VALUE; | |
} else if(c===',') { | |
if(parser.state === CLOSE_OBJECT) | |
parser.stack.push(CLOSE_OBJECT); | |
closeValue(parser); | |
parser.state = OPEN_KEY; | |
} else error(parser, 'Bad object'); | |
continue; | |
case OPEN_ARRAY: // after an array there always a value | |
case VALUE: | |
if (c === '\r' || c === '\n' || c === ' ' || c === '\t') continue; | |
if(parser.state===OPEN_ARRAY) { | |
emit(parser, 'onopenarray'); | |
this.depth++; | |
parser.state = VALUE; | |
if(c === ']') { | |
emit(parser, 'onclosearray'); | |
this.depth--; | |
parser.state = parser.stack.pop() || VALUE; | |
continue; | |
} else { | |
parser.stack.push(CLOSE_ARRAY); | |
} | |
} | |
if(c === '"') parser.state = STRING; | |
else if(c === '{') parser.state = OPEN_OBJECT; | |
else if(c === '[') parser.state = OPEN_ARRAY; | |
else if(c === 't') parser.state = TRUE; | |
else if(c === 'f') parser.state = FALSE; | |
else if(c === 'n') parser.state = NULL; | |
else if(c === '-') { // keep and continue | |
parser.numberNode += c; | |
} else if(c==='0') { | |
parser.numberNode += c; | |
parser.state = NUMBER_DIGIT; | |
} else if('123456789'.indexOf(c) !== -1) { | |
parser.numberNode += c; | |
parser.state = NUMBER_DIGIT; | |
} else error(parser, "Bad value"); | |
continue; | |
case CLOSE_ARRAY: | |
if(c===',') { | |
parser.stack.push(CLOSE_ARRAY); | |
closeValue(parser, 'onvalue'); | |
parser.state = VALUE; | |
} else if (c===']') { | |
emitNode(parser, 'onclosearray'); | |
this.depth--; | |
parser.state = parser.stack.pop() || VALUE; | |
} else if (c === '\r' || c === '\n' || c === ' ' || c === '\t') | |
continue; | |
else error(parser, 'Bad array'); | |
continue; | |
case STRING: | |
// thanks thejh, this is an about 50% performance improvement. | |
var starti = i-1 | |
, slashed = parser.slashed | |
, unicodeI = parser.unicodeI | |
; | |
STRING_BIGLOOP: while (true) { | |
// zero means "no unicode active". 1-4 mean "parse some more". end after 4. | |
while (unicodeI > 0) { | |
parser.unicodeS += c; | |
c = chunk.charAt(i++); | |
if (unicodeI === 4) { | |
// TODO this might be slow? well, probably not used too often anyway | |
parser.textNode += String.fromCharCode(parseInt(parser.unicodeS, 16)); | |
unicodeI = 0; | |
starti = i-1; | |
} else { | |
unicodeI++; | |
} | |
// we can just break here: no stuff we skipped that still has to be sliced out or so | |
if (!c) break STRING_BIGLOOP; | |
} | |
if (c === '"' && !slashed) { | |
parser.state = parser.stack.pop() || VALUE; | |
parser.textNode += chunk.substring(starti, i-1); | |
if(!parser.textNode) { | |
emit(parser, "onvalue", ""); | |
} | |
break; | |
} | |
if (c === '\\' && !slashed) { | |
slashed = true; | |
parser.textNode += chunk.substring(starti, i-1); | |
c = chunk.charAt(i++); | |
if (!c) break; | |
} | |
if (slashed) { | |
slashed = false; | |
if (c === 'n') { parser.textNode += '\n'; } | |
else if (c === 'r') { parser.textNode += '\r'; } | |
else if (c === 't') { parser.textNode += '\t'; } | |
else if (c === 'f') { parser.textNode += '\f'; } | |
else if (c === 'b') { parser.textNode += '\b'; } | |
else if (c === 'u') { | |
// \uxxxx. meh! | |
unicodeI = 1; | |
parser.unicodeS = ''; | |
} else { | |
parser.textNode += c; | |
} | |
c = chunk.charAt(i++); | |
starti = i-1; | |
if (!c) break; | |
else continue; | |
} | |
stringTokenPattern.lastIndex = i; | |
var reResult = stringTokenPattern.exec(chunk); | |
if (reResult === null) { | |
i = chunk.length+1; | |
parser.textNode += chunk.substring(starti, i-1); | |
break; | |
} | |
i = reResult.index+1; | |
c = chunk.charAt(reResult.index); | |
if (!c) { | |
parser.textNode += chunk.substring(starti, i-1); | |
break; | |
} | |
} | |
parser.slashed = slashed; | |
parser.unicodeI = unicodeI; | |
continue; | |
case TRUE: | |
if (c==='') continue; // strange buffers | |
if (c==='r') parser.state = TRUE2; | |
else error(parser, 'Invalid true started with t'+ c); | |
continue; | |
case TRUE2: | |
if (c==='') continue; | |
if (c==='u') parser.state = TRUE3; | |
else error(parser, 'Invalid true started with tr'+ c); | |
continue; | |
case TRUE3: | |
if (c==='') continue; | |
if(c==='e') { | |
emit(parser, "onvalue", true); | |
parser.state = parser.stack.pop() || VALUE; | |
} else error(parser, 'Invalid true started with tru'+ c); | |
continue; | |
case FALSE: | |
if (c==='') continue; | |
if (c==='a') parser.state = FALSE2; | |
else error(parser, 'Invalid false started with f'+ c); | |
continue; | |
case FALSE2: | |
if (c==='') continue; | |
if (c==='l') parser.state = FALSE3; | |
else error(parser, 'Invalid false started with fa'+ c); | |
continue; | |
case FALSE3: | |
if (c==='') continue; | |
if (c==='s') parser.state = FALSE4; | |
else error(parser, 'Invalid false started with fal'+ c); | |
continue; | |
case FALSE4: | |
if (c==='') continue; | |
if (c==='e') { | |
emit(parser, "onvalue", false); | |
parser.state = parser.stack.pop() || VALUE; | |
} else error(parser, 'Invalid false started with fals'+ c); | |
continue; | |
case NULL: | |
if (c==='') continue; | |
if (c==='u') parser.state = NULL2; | |
else error(parser, 'Invalid null started with n'+ c); | |
continue; | |
case NULL2: | |
if (c==='') continue; | |
if (c==='l') parser.state = NULL3; | |
else error(parser, 'Invalid null started with nu'+ c); | |
continue; | |
case NULL3: | |
if (c==='') continue; | |
if(c==='l') { | |
emit(parser, "onvalue", null); | |
parser.state = parser.stack.pop() || VALUE; | |
} else error(parser, 'Invalid null started with nul'+ c); | |
continue; | |
case NUMBER_DECIMAL_POINT: | |
if(c==='.') { | |
parser.numberNode += c; | |
parser.state = NUMBER_DIGIT; | |
} else error(parser, 'Leading zero not followed by .'); | |
continue; | |
case NUMBER_DIGIT: | |
if('0123456789'.indexOf(c) !== -1) parser.numberNode += c; | |
else if (c==='.') { | |
if(parser.numberNode.indexOf('.')!==-1) | |
error(parser, 'Invalid number has two dots'); | |
parser.numberNode += c; | |
} else if (c==='e' || c==='E') { | |
if(parser.numberNode.indexOf('e')!==-1 || | |
parser.numberNode.indexOf('E')!==-1 ) | |
error(parser, 'Invalid number has two exponential'); | |
parser.numberNode += c; | |
} else if (c==="+" || c==="-") { | |
if(!(p==='e' || p==='E')) | |
error(parser, 'Invalid symbol in number'); | |
parser.numberNode += c; | |
} else { | |
closeNumber(parser); | |
i--; // go back one | |
parser.state = parser.stack.pop() || VALUE; | |
} | |
continue; | |
default: | |
error(parser, "Unknown state: " + parser.state); | |
} | |
} | |
if (parser.position >= parser.bufferCheckPosition) | |
checkBufferLength(parser); | |
return parser; | |
} | |
return clarinet; | |
})(); | |
/** | |
* A bridge used to assign stateless functions to listen to clarinet. | |
* | |
* As well as the parameter from clarinet, each callback will also be passed | |
* the result of the last callback. | |
* | |
* This may also be used to clear all listeners by assigning zero handlers: | |
* | |
* clarinetListenerAdaptor( clarinet, {} ) | |
*/ | |
function clarinetListenerAdaptor(clarinetParser, handlers){ | |
var state; | |
clarinet.EVENTS.forEach(function(eventName){ | |
var handlerFunction = handlers[eventName]; | |
clarinetParser['on'+eventName] = handlerFunction && | |
function(param) { | |
state = handlerFunction( state, param); | |
}; | |
}); | |
} | |
// based on gist https://gist.github.com/monsur/706839 | |
/** | |
* XmlHttpRequest's getAllResponseHeaders() method returns a string of response | |
* headers according to the format described here: | |
* http://www.w3.org/TR/XMLHttpRequest/#the-getallresponseheaders-method | |
* This method parses that string into a user-friendly key/value pair object. | |
*/ | |
function parseResponseHeaders(headerStr) { | |
var headers = {}; | |
headerStr && headerStr.split('\u000d\u000a') | |
.forEach(function(headerPair){ | |
// Can't use split() here because it does the wrong thing | |
// if the header value has the string ": " in it. | |
var index = headerPair.indexOf('\u003a\u0020'); | |
headers[headerPair.substring(0, index)] | |
= headerPair.substring(index + 2); | |
}); | |
return headers; | |
} | |
function httpTransport(){ | |
return new XMLHttpRequest(); | |
} | |
/** | |
* A wrapper around the browser XmlHttpRequest object that raises an | |
* event whenever a new part of the response is available. | |
* | |
* In older browsers progressive reading is impossible so all the | |
* content is given in a single call. For newer ones several events | |
* should be raised, allowing progressive interpretation of the response. | |
* | |
* @param {Function} oboeBus an event bus local to this Oboe instance | |
* @param {XMLHttpRequest} xhr the xhr to use as the transport. Under normal | |
* operation, will have been created using httpTransport() above | |
* but for tests a stub can be provided instead. | |
* @param {String} method one of 'GET' 'POST' 'PUT' 'PATCH' 'DELETE' | |
* @param {String} url the url to make a request to | |
* @param {String|Null} data some content to be sent with the request. | |
* Only valid if method is POST or PUT. | |
* @param {Object} [headers] the http request headers to send | |
*/ | |
function streamingHttp(oboeBus, xhr, method, url, data, headers) { | |
var emitStreamData = oboeBus(STREAM_DATA).emit, | |
emitFail = oboeBus(FAIL_EVENT).emit, | |
numberOfCharsAlreadyGivenToCallback = 0; | |
// When an ABORTING message is put on the event bus abort | |
// the ajax request | |
oboeBus( ABORTING ).on( function(){ | |
// if we keep the onreadystatechange while aborting the XHR gives | |
// a callback like a successful call so first remove this listener | |
// by assigning null: | |
xhr.onreadystatechange = null; | |
xhr.abort(); | |
}); | |
/** | |
* Handle input from the underlying xhr: either a state change, | |
* the progress event or the request being complete. | |
*/ | |
function handleProgress() { | |
var textSoFar = xhr.responseText, | |
newText = textSoFar.substr(numberOfCharsAlreadyGivenToCallback); | |
/* Raise the event for new text. | |
On older browsers, the new text is the whole response. | |
On newer/better ones, the fragment part that we got since | |
last progress. */ | |
if( newText ) { | |
emitStreamData( newText ); | |
} | |
numberOfCharsAlreadyGivenToCallback = len(textSoFar); | |
} | |
if('onprogress' in xhr){ // detect browser support for progressive delivery | |
xhr.onprogress = handleProgress; | |
} | |
xhr.onreadystatechange = function() { | |
switch( xhr.readyState ) { | |
case 2: | |
oboeBus( HTTP_START ).emit( | |
xhr.status, | |
parseResponseHeaders(xhr.getAllResponseHeaders()) ); | |
return; | |
case 4: | |
// is this a 2xx http code? | |
var successful = String(xhr.status)[0] == 2; | |
if( successful ) { | |
// In Chrome 29 (not 28) no onprogress is emitted when a response | |
// is complete before the onload. We need to always do handleInput | |
// in case we get the load but have not had a final progress event. | |
// This looks like a bug and may change in future but let's take | |
// the safest approach and assume we might not have received a | |
// progress event for each part of the response | |
handleProgress(); | |
oboeBus(STREAM_END).emit(); | |
} else { | |
emitFail( errorReport( | |
xhr.status, | |
xhr.responseText | |
)); | |
} | |
} | |
}; | |
try{ | |
xhr.open(method, url, true); | |
for( var headerName in headers ){ | |
xhr.setRequestHeader(headerName, headers[headerName]); | |
} | |
xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); | |
xhr.send(data); | |
} catch( e ) { | |
// To keep a consistent interface with Node, we can't emit an event here. | |
// Node's streaming http adaptor receives the error as an asynchronous | |
// event rather than as an exception. If we emitted now, the Oboe user | |
// has had no chance to add a .fail listener so there is no way | |
// the event could be useful. For both these reasons defer the | |
// firing to the next JS frame. | |
window.setTimeout( | |
partialComplete(emitFail, errorReport(undefined, undefined, e)) | |
, 0 | |
); | |
} | |
} | |
var jsonPathSyntax = (function() { | |
var | |
/** | |
* Export a regular expression as a simple function by exposing just | |
* the Regex#exec. This allows regex tests to be used under the same | |
* interface as differently implemented tests, or for a user of the | |
* tests to not concern themselves with their implementation as regular | |
* expressions. | |
* | |
* This could also be expressed point-free as: | |
* Function.prototype.bind.bind(RegExp.prototype.exec), | |
* | |
* But that's far too confusing! (and not even smaller once minified | |
* and gzipped) | |
*/ | |
regexDescriptor = function regexDescriptor(regex) { | |
return regex.exec.bind(regex); | |
} | |
/** | |
* Join several regular expressions and express as a function. | |
* This allows the token patterns to reuse component regular expressions | |
* instead of being expressed in full using huge and confusing regular | |
* expressions. | |
*/ | |
, jsonPathClause = varArgs(function( componentRegexes ) { | |
// The regular expressions all start with ^ because we | |
// only want to find matches at the start of the | |
// JSONPath fragment we are inspecting | |
componentRegexes.unshift(/^/); | |
return regexDescriptor( | |
RegExp( | |
componentRegexes.map(attr('source')).join('') | |
) | |
); | |
}) | |
, possiblyCapturing = /(\$?)/ | |
, namedNode = /([\w-_]+|\*)/ | |
, namePlaceholder = /()/ | |
, nodeInArrayNotation = /\["([^"]+)"\]/ | |
, numberedNodeInArrayNotation = /\[(\d+|\*)\]/ | |
, fieldList = /{([\w ]*?)}/ | |
, optionalFieldList = /(?:{([\w ]*?)})?/ | |
// foo or * | |
, jsonPathNamedNodeInObjectNotation = jsonPathClause( | |
possiblyCapturing, | |
namedNode, | |
optionalFieldList | |
) | |
// ["foo"] | |
, jsonPathNamedNodeInArrayNotation = jsonPathClause( | |
possiblyCapturing, | |
nodeInArrayNotation, | |
optionalFieldList | |
) | |
// [2] or [*] | |
, jsonPathNumberedNodeInArrayNotation = jsonPathClause( | |
possiblyCapturing, | |
numberedNodeInArrayNotation, | |
optionalFieldList | |
) | |
// {a b c} | |
, jsonPathPureDuckTyping = jsonPathClause( | |
possiblyCapturing, | |
namePlaceholder, | |
fieldList | |
) | |
// .. | |
, jsonPathDoubleDot = jsonPathClause(/\.\./) | |
// . | |
, jsonPathDot = jsonPathClause(/\./) | |
// ! | |
, jsonPathBang = jsonPathClause( | |
possiblyCapturing, | |
/!/ | |
) | |
// nada! | |
, emptyString = jsonPathClause(/$/) | |
; | |
/* We export only a single function. When called, this function injects | |
into another function the descriptors from above. | |
*/ | |
return function (fn){ | |
return fn( | |
lazyUnion( | |
jsonPathNamedNodeInObjectNotation | |
, jsonPathNamedNodeInArrayNotation | |
, jsonPathNumberedNodeInArrayNotation | |
, jsonPathPureDuckTyping | |
) | |
, jsonPathDoubleDot | |
, jsonPathDot | |
, jsonPathBang | |
, emptyString | |
); | |
}; | |
}()); | |
/** | |
* Get a new key->node mapping | |
* | |
* @param {String|Number} key | |
* @param {Object|Array|String|Number|null} node a value found in the json | |
*/ | |
function namedNode(key, node) { | |
return {key:key, node:node}; | |
} | |
/** get the key of a namedNode */ | |
var keyOf = attr('key'); | |
/** get the node from a namedNode */ | |
var nodeOf = attr('node'); | |
/** | |
* This file provides various listeners which can be used to build up | |
* a changing ascent based on the callbacks provided by Clarinet. It listens | |
* to the low-level events from Clarinet and emits higher-level ones. | |
* | |
* The building up is stateless so to track a JSON file | |
* clarinetListenerAdaptor.js is required to store the ascent state | |
* between calls. | |
*/ | |
/** | |
* A special value to use in the path list to represent the path 'to' a root | |
* object (which doesn't really have any path). This prevents the need for | |
* special-casing detection of the root object and allows it to be treated | |
* like any other object. We might think of this as being similar to the | |
* 'unnamed root' domain ".", eg if I go to | |
* http://en.wikipedia.org./wiki/En/Main_page the dot after 'org' deliminates | |
* the unnamed root of the DNS. | |
* | |
* This is kept as an object to take advantage that in Javascript's OO objects | |
* are guaranteed to be distinct, therefore no other object can possibly clash | |
* with this one. Strings, numbers etc provide no such guarantee. | |
**/ | |
var ROOT_PATH = {}; | |
/** | |
* Create a new set of handlers for clarinet's events, bound to the emit | |
* function given. | |
*/ | |
function incrementalContentBuilder( oboeBus ) { | |
var emitNodeFound = oboeBus(NODE_FOUND).emit, | |
emitRootFound = oboeBus(ROOT_FOUND).emit, | |
emitPathFound = oboeBus(PATH_FOUND).emit; | |
function arrayIndicesAreKeys( possiblyInconsistentAscent, newDeepestNode) { | |
/* for values in arrays we aren't pre-warned of the coming paths | |
(Clarinet gives no call to onkey like it does for values in objects) | |
so if we are in an array we need to create this path ourselves. The | |
key will be len(parentNode) because array keys are always sequential | |
numbers. */ | |
var parentNode = nodeOf( head( possiblyInconsistentAscent)); | |
return isOfType( Array, parentNode) | |
? | |
pathFound( possiblyInconsistentAscent, | |
len(parentNode), | |
newDeepestNode | |
) | |
: | |
// nothing needed, return unchanged | |
possiblyInconsistentAscent | |
; | |
} | |
function nodeFound( ascent, newDeepestNode ) { | |
if( !ascent ) { | |
// we discovered the root node, | |
emitRootFound( newDeepestNode); | |
return pathFound( ascent, ROOT_PATH, newDeepestNode); | |
} | |
// we discovered a non-root node | |
var arrayConsistentAscent = arrayIndicesAreKeys( ascent, newDeepestNode), | |
ancestorBranches = tail( arrayConsistentAscent), | |
previouslyUnmappedName = keyOf( head( arrayConsistentAscent)); | |
appendBuiltContent( | |
ancestorBranches, | |
previouslyUnmappedName, | |
newDeepestNode | |
); | |
return cons( | |
namedNode( previouslyUnmappedName, newDeepestNode ), | |
ancestorBranches | |
); | |
} | |
/** | |
* Add a new value to the object we are building up to represent the | |
* parsed JSON | |
*/ | |
function appendBuiltContent( ancestorBranches, key, node ){ | |
nodeOf( head( ancestorBranches))[key] = node; | |
} | |
/** | |
* For when we find a new key in the json. | |
* | |
* @param {String|Number|Object} newDeepestName the key. If we are in an | |
* array will be a number, otherwise a string. May take the special | |
* value ROOT_PATH if the root node has just been found | |
* | |
* @param {String|Number|Object|Array|Null|undefined} [maybeNewDeepestNode] | |
* usually this won't be known so can be undefined. Can't use null | |
* to represent unknown because null is a valid value in JSON | |
**/ | |
function pathFound(ascent, newDeepestName, maybeNewDeepestNode) { | |
if( ascent ) { // if not root | |
// If we have the key but (unless adding to an array) no known value | |
// yet. Put that key in the output but against no defined value: | |
appendBuiltContent( ascent, newDeepestName, maybeNewDeepestNode ); | |
} | |
var ascentWithNewPath = cons( | |
namedNode( newDeepestName, | |
maybeNewDeepestNode), | |
ascent | |
); | |
emitPathFound( ascentWithNewPath); | |
return ascentWithNewPath; | |
} | |
/** | |
* For when the current node ends | |
*/ | |
function nodeFinished( ascent ) { | |
emitNodeFound( ascent); | |
// pop the complete node and its path off the list: | |
return tail( ascent); | |
} | |
return { | |
openobject : function (ascent, firstKey) { | |
var ascentAfterNodeFound = nodeFound(ascent, {}); | |
/* It is a perculiarity of Clarinet that for non-empty objects it | |
gives the first key with the openobject event instead of | |
in a subsequent key event. | |
firstKey could be the empty string in a JSON object like | |
{'':'foo'} which is technically valid. | |
So can't check with !firstKey, have to see if has any | |
defined value. */ | |
return defined(firstKey) | |
? | |
/* We know the first key of the newly parsed object. Notify that | |
path has been found but don't put firstKey permanently onto | |
pathList yet because we haven't identified what is at that key | |
yet. Give null as the value because we haven't seen that far | |
into the json yet */ | |
pathFound(ascentAfterNodeFound, firstKey) | |
: | |
ascentAfterNodeFound | |
; | |
}, | |
openarray: function (ascent) { | |
return nodeFound(ascent, []); | |
}, | |
// called by Clarinet when keys are found in objects | |
key: pathFound, | |
/* Emitted by Clarinet when primitive values are found, ie Strings, | |
Numbers, and null. | |
Because these are always leaves in the JSON, we find and finish the | |
node in one step, expressed as functional composition: */ | |
value: compose2( nodeFinished, nodeFound ), | |
// we make no distinction in how we handle object and arrays closing. | |
// For both, interpret as the end of the current node. | |
closeobject: nodeFinished, | |
closearray: nodeFinished | |
}; | |
} | |
/** | |
* The jsonPath evaluator compiler used for Oboe.js. | |
* | |
* One function is exposed. This function takes a String JSONPath spec and | |
* returns a function to test candidate ascents for matches. | |
* | |
* String jsonPath -> (List ascent) -> Boolean|Object | |
* | |
* This file is coded in a pure functional style. That is, no function has | |
* side effects, every function evaluates to the same value for the same | |
* arguments and no variables are reassigned. | |
*/ | |
// the call to jsonPathSyntax injects the token syntaxes that are needed | |
// inside the compiler | |
var jsonPathCompiler = jsonPathSyntax(function (pathNodeSyntax, | |
doubleDotSyntax, | |
dotSyntax, | |
bangSyntax, | |
emptySyntax ) { | |
var CAPTURING_INDEX = 1; | |
var NAME_INDEX = 2; | |
var FIELD_LIST_INDEX = 3; | |
var headKey = compose2(keyOf, head), | |
headNode = compose2(nodeOf, head); | |
/** | |
* Create an evaluator function for a named path node, expressed in the | |
* JSONPath like: | |
* foo | |
* ["bar"] | |
* [2] | |
*/ | |
function nameClause(previousExpr, detection ) { | |
var name = detection[NAME_INDEX], | |
matchesName = ( !name || name == '*' ) | |
? always | |
: function(ascent){return headKey(ascent) == name}; | |
return lazyIntersection(matchesName, previousExpr); | |
} | |
/** | |
* Create an evaluator function for a a duck-typed node, expressed like: | |
* | |
* {spin, taste, colour} | |
* .particle{spin, taste, colour} | |
* *{spin, taste, colour} | |
*/ | |
function duckTypeClause(previousExpr, detection) { | |
var fieldListStr = detection[FIELD_LIST_INDEX]; | |
if (!fieldListStr) | |
return previousExpr; // don't wrap at all, return given expr as-is | |
var hasAllrequiredFields = partialComplete( | |
hasAllProperties, | |
arrayAsList(fieldListStr.split(/\W+/)) | |
), | |
isMatch = compose2( | |
hasAllrequiredFields, | |
headNode | |
); | |
return lazyIntersection(isMatch, previousExpr); | |
} | |
/** | |
* Expression for $, returns the evaluator function | |
*/ | |
function capture( previousExpr, detection ) { | |
// extract meaning from the detection | |
var capturing = !!detection[CAPTURING_INDEX]; | |
if (!capturing) | |
return previousExpr; // don't wrap at all, return given expr as-is | |
return lazyIntersection(previousExpr, head); | |
} | |
/** | |
* Create an evaluator function that moves onto the next item on the | |
* lists. This function is the place where the logic to move up a | |
* level in the ascent exists. | |
* | |
* Eg, for JSONPath ".foo" we need skip1(nameClause(always, [,'foo'])) | |
*/ | |
function skip1(previousExpr) { | |
if( previousExpr == always ) { | |
/* If there is no previous expression this consume command | |
is at the start of the jsonPath. | |
Since JSONPath specifies what we'd like to find but not | |
necessarily everything leading down to it, when running | |
out of JSONPath to check against we default to true */ | |
return always; | |
} | |
/** return true if the ascent we have contains only the JSON root, | |
* false otherwise | |
*/ | |
function notAtRoot(ascent){ | |
return headKey(ascent) != ROOT_PATH; | |
} | |
return lazyIntersection( | |
/* If we're already at the root but there are more | |
expressions to satisfy, can't consume any more. No match. | |
This check is why none of the other exprs have to be able | |
to handle empty lists; skip1 is the only evaluator that | |
moves onto the next token and it refuses to do so once it | |
reaches the last item in the list. */ | |
notAtRoot, | |
/* We are not at the root of the ascent yet. | |
Move to the next level of the ascent by handing only | |
the tail to the previous expression */ | |
compose2(previousExpr, tail) | |
); | |
} | |
/** | |
* Create an evaluator function for the .. (double dot) token. Consumes | |
* zero or more levels of the ascent, the fewest that are required to find | |
* a match when given to previousExpr. | |
*/ | |
function skipMany(previousExpr) { | |
if( previousExpr == always ) { | |
/* If there is no previous expression this consume command | |
is at the start of the jsonPath. | |
Since JSONPath specifies what we'd like to find but not | |
necessarily everything leading down to it, when running | |
out of JSONPath to check against we default to true */ | |
return always; | |
} | |
var | |
// In JSONPath .. is equivalent to !.. so if .. reaches the root | |
// the match has succeeded. Ie, we might write ..foo or !..foo | |
// and both should match identically. | |
terminalCaseWhenArrivingAtRoot = rootExpr(), | |
terminalCaseWhenPreviousExpressionIsSatisfied = previousExpr, | |
recursiveCase = skip1(skipManyInner), | |
cases = lazyUnion( | |
terminalCaseWhenArrivingAtRoot | |
, terminalCaseWhenPreviousExpressionIsSatisfied | |
, recursiveCase | |
); | |
function skipManyInner(ascent) { | |
if( !ascent ) { | |
// have gone past the start, not a match: | |
return false; | |
} | |
return cases(ascent); | |
} | |
return skipManyInner; | |
} | |
/** | |
* Generate an evaluator for ! - matches only the root element of the json | |
* and ignores any previous expressions since nothing may precede !. | |
*/ | |
function rootExpr() { | |
return function(ascent){ | |
return headKey(ascent) == ROOT_PATH; | |
}; | |
} | |
/** | |
* Generate a statement wrapper to sit around the outermost | |
* clause evaluator. | |
* | |
* Handles the case where the capturing is implicit because the JSONPath | |
* did not contain a '$' by returning the last node. | |
*/ | |
function statementExpr(lastClause) { | |
return function(ascent) { | |
// kick off the evaluation by passing through to the last clause | |
var exprMatch = lastClause(ascent); | |
return exprMatch === true ? head(ascent) : exprMatch; | |
}; | |
} | |
/** | |
* For when a token has been found in the JSONPath input. | |
* Compiles the parser for that token and returns in combination with the | |
* parser already generated. | |
* | |
* @param {Function} exprs a list of the clause evaluator generators for | |
* the token that was found | |
* @param {Function} parserGeneratedSoFar the parser already found | |
* @param {Array} detection the match given by the regex engine when | |
* the feature was found | |
*/ | |
function expressionsReader( exprs, parserGeneratedSoFar, detection ) { | |
// if exprs is zero-length foldR will pass back the | |
// parserGeneratedSoFar as-is so we don't need to treat | |
// this as a special case | |
return foldR( | |
function( parserGeneratedSoFar, expr ){ | |
return expr(parserGeneratedSoFar, detection); | |
}, | |
parserGeneratedSoFar, | |
exprs | |
); | |
} | |
/** | |
* If jsonPath matches the given detector function, creates a function which | |
* evaluates against every clause in the clauseEvaluatorGenerators. The | |
* created function is propagated to the onSuccess function, along with | |
* the remaining unparsed JSONPath substring. | |
* | |
* The intended use is to create a clauseMatcher by filling in | |
* the first two arguments, thus providing a function that knows | |
* some syntax to match and what kind of generator to create if it | |
* finds it. The parameter list once completed is: | |
* | |
* (jsonPath, parserGeneratedSoFar, onSuccess) | |
* | |
* onSuccess may be compileJsonPathToFunction, to recursively continue | |
* parsing after finding a match or returnFoundParser to stop here. | |
*/ | |
function generateClauseReaderIfTokenFound ( | |
tokenDetector, clauseEvaluatorGenerators, | |
jsonPath, parserGeneratedSoFar, onSuccess) { | |
var detected = tokenDetector(jsonPath); | |
if(detected) { | |
var compiledParser = expressionsReader( | |
clauseEvaluatorGenerators, | |
parserGeneratedSoFar, | |
detected | |
), | |
remainingUnparsedJsonPath = jsonPath.substr(len(detected[0])); | |
return onSuccess(remainingUnparsedJsonPath, compiledParser); | |
} | |
} | |
/** | |
* Partially completes generateClauseReaderIfTokenFound above. | |
*/ | |
function clauseMatcher(tokenDetector, exprs) { | |
return partialComplete( | |
generateClauseReaderIfTokenFound, | |
tokenDetector, | |
exprs | |
); | |
} | |
/** | |
* clauseForJsonPath is a function which attempts to match against | |
* several clause matchers in order until one matches. If non match the | |
* jsonPath expression is invalid and an error is thrown. | |
* | |
* The parameter list is the same as a single clauseMatcher: | |
* | |
* (jsonPath, parserGeneratedSoFar, onSuccess) | |
*/ | |
var clauseForJsonPath = lazyUnion( | |
clauseMatcher(pathNodeSyntax , list( capture, | |
duckTypeClause, | |
nameClause, | |
skip1 )) | |
, clauseMatcher(doubleDotSyntax , list( skipMany)) | |
// dot is a separator only (like whitespace in other languages) but | |
// rather than make it a special case, use an empty list of | |
// expressions when this token is found | |
, clauseMatcher(dotSyntax , list() ) | |
, clauseMatcher(bangSyntax , list( capture, | |
rootExpr)) | |
, clauseMatcher(emptySyntax , list( statementExpr)) | |
, function (jsonPath) { | |
throw Error('"' + jsonPath + '" could not be tokenised') | |
} | |
); | |
/** | |
* One of two possible values for the onSuccess argument of | |
* generateClauseReaderIfTokenFound. | |
* | |
* When this function is used, generateClauseReaderIfTokenFound simply | |
* returns the compiledParser that it made, regardless of if there is | |
* any remaining jsonPath to be compiled. | |
*/ | |
function returnFoundParser(_remainingJsonPath, compiledParser){ | |
return compiledParser | |
} | |
/** | |
* Recursively compile a JSONPath expression. | |
* | |
* This function serves as one of two possible values for the onSuccess | |
* argument of generateClauseReaderIfTokenFound, meaning continue to | |
* recursively compile. Otherwise, returnFoundParser is given and | |
* compilation terminates. | |
*/ | |
function compileJsonPathToFunction( uncompiledJsonPath, | |
parserGeneratedSoFar ) { | |
/** | |
* On finding a match, if there is remaining text to be compiled | |
* we want to either continue parsing using a recursive call to | |
* compileJsonPathToFunction. Otherwise, we want to stop and return | |
* the parser that we have found so far. | |
*/ | |
var onFind = uncompiledJsonPath | |
? compileJsonPathToFunction | |
: returnFoundParser; | |
return clauseForJsonPath( | |
uncompiledJsonPath, | |
parserGeneratedSoFar, | |
onFind | |
); | |
} | |
/** | |
* This is the function that we expose to the rest of the library. | |
*/ | |
return function(jsonPath){ | |
try { | |
// Kick off the recursive parsing of the jsonPath | |
return compileJsonPathToFunction(jsonPath, always); | |
} catch( e ) { | |
throw Error( 'Could not compile "' + jsonPath + | |
'" because ' + e.message | |
); | |
} | |
} | |
}); | |
/** | |
* A pub/sub which is responsible for a single event type. A | |
* multi-event type event bus is created by pubSub by collecting | |
* several of these. | |
* | |
* @param {String} eventType | |
* the name of the events managed by this singleEventPubSub | |
* @param {singleEventPubSub} [newListener] | |
* place to notify of new listeners | |
* @param {singleEventPubSub} [removeListener] | |
* place to notify of when listeners are removed | |
*/ | |
function singleEventPubSub(eventType, newListener, removeListener){ | |
/** we are optimised for emitting events over firing them. | |
* As well as the tuple list which stores event ids and | |
* listeners there is a list with just the listeners which | |
* can be iterated more quickly when we are emitting | |
*/ | |
var listenerTupleList, | |
listenerList; | |
function hasId(id){ | |
return function(tuple) { | |
return tuple.id == id; | |
}; | |
} | |
return { | |
/** | |
* @param {Function} listener | |
* @param {*} listenerId | |
* an id that this listener can later by removed by. | |
* Can be of any type, to be compared to other ids using == | |
*/ | |
on:function( listener, listenerId ) { | |
var tuple = { | |
listener: listener | |
, id: listenerId || listener // when no id is given use the | |
// listener function as the id | |
}; | |
if( newListener ) { | |
newListener.emit(eventType, listener, tuple.id); | |
} | |
listenerTupleList = cons( tuple, listenerTupleList ); | |
listenerList = cons( listener, listenerList ); | |
return this; // chaining | |
}, | |
emit:function () { | |
applyEach( listenerList, arguments ); | |
}, | |
un: function( listenerId ) { | |
var removed; | |
listenerTupleList = without( | |
listenerTupleList, | |
hasId(listenerId), | |
function(tuple){ | |
removed = tuple; | |
} | |
); | |
if( removed ) { | |
listenerList = without( listenerList, function(listener){ | |
return listener == removed.listener; | |
}); | |
if( removeListener ) { | |
removeListener.emit(eventType, removed.listener, removed.id); | |
} | |
} | |
}, | |
listeners: function(){ | |
// differs from Node EventEmitter: returns list, not array | |
return listenerList; | |
}, | |
hasListener: function(listenerId){ | |
var test = listenerId? hasId(listenerId) : always; | |
return defined(first( test, listenerTupleList)); | |
} | |
}; | |
} | |
/** | |
* pubSub is a curried interface for listening to and emitting | |
* events. | |
* | |
* If we get a bus: | |
* | |
* var bus = pubSub(); | |
* | |
* We can listen to event 'foo' like: | |
* | |
* bus('foo').on(myCallback) | |
* | |
* And emit event foo like: | |
* | |
* bus('foo').emit() | |
* | |
* or, with a parameter: | |
* | |
* bus('foo').emit('bar') | |
* | |
* All functions can be cached and don't need to be | |
* bound. Ie: | |
* | |
* var fooEmitter = bus('foo').emit | |
* fooEmitter('bar'); // emit an event | |
* fooEmitter('baz'); // emit another | |
* | |
* There's also an uncurried[1] shortcut for .emit and .on: | |
* | |
* bus.on('foo', callback) | |
* bus.emit('foo', 'bar') | |
* | |
* [1]: http://zvon.org/other/haskell/Outputprelude/uncurry_f.html | |
*/ | |
function pubSub(){ | |
var singles = {}, | |
newListener = newSingle('newListener'), | |
removeListener = newSingle('removeListener'); | |
function newSingle(eventName) { | |
return singles[eventName] = singleEventPubSub( | |
eventName, | |
newListener, | |
removeListener | |
); | |
} | |
/** pubSub instances are functions */ | |
function pubSubInstance( eventName ){ | |
return singles[eventName] || newSingle( eventName ); | |
} | |
// add convenience EventEmitter-style uncurried form of 'emit' and 'on' | |
['emit', 'on', 'un'].forEach(function(methodName){ | |
pubSubInstance[methodName] = varArgs(function(eventName, parameters){ | |
apply( parameters, pubSubInstance( eventName )[methodName]); | |
}); | |
}); | |
return pubSubInstance; | |
} | |
/** | |
* This file declares some constants to use as names for event types. | |
*/ | |
var // the events which are never exported are kept as | |
// the smallest possible representation, in numbers: | |
_S = 1, | |
// fired whenever a node is found in the JSON: | |
NODE_FOUND = _S++, | |
// fired whenever a path is found in the JSON: | |
PATH_FOUND = _S++, | |
FAIL_EVENT = 'fail', | |
ROOT_FOUND = _S++, | |
HTTP_START = 'start', | |
STREAM_DATA = 'content', | |
STREAM_END = _S++, | |
ABORTING = _S++; | |
function errorReport(statusCode, body, error) { | |
try{ | |
var jsonBody = JSON.parse(body); | |
}catch(e){} | |
return { | |
statusCode:statusCode, | |
body:body, | |
jsonBody:jsonBody, | |
thrown:error | |
}; | |
} | |
/** | |
* The pattern adaptor listens for newListener and removeListener | |
* events. When patterns are added or removed it compiles the JSONPath | |
* and wires them up. | |
* | |
* When nodes and paths are found it emits the fully-qualified match | |
* events with parameters ready to ship to the outside world | |
*/ | |
function patternAdapter(oboeBus, jsonPathCompiler) { | |
var predicateEventMap = { | |
node:oboeBus(NODE_FOUND) | |
, path:oboeBus(PATH_FOUND) | |
}; | |
function emitMatchingNode(emitMatch, node, ascent) { | |
/* | |
We're now calling to the outside world where Lisp-style | |
lists will not be familiar. Convert to standard arrays. | |
Also, reverse the order because it is more common to | |
list paths "root to leaf" than "leaf to root" */ | |
var descent = reverseList(ascent); | |
emitMatch( | |
node, | |
// To make a path, strip off the last item which is the special | |
// ROOT_PATH token for the 'path' to the root node | |
listAsArray(tail(map(keyOf,descent))), // path | |
listAsArray(map(nodeOf, descent)) // ancestors | |
); | |
} | |
/* | |
* Set up the catching of events such as NODE_FOUND and PATH_FOUND and, if | |
* matching the specified pattern, propagate to pattern-match events such as | |
* oboeBus('node:!') | |
* | |
* | |
* | |
* @param {Function} predicateEvent | |
* either oboeBus(NODE_FOUND) or oboeBus(PATH_FOUND). | |
* @param {Function} compiledJsonPath | |
*/ | |
function addUnderlyingListener( fullEventName, predicateEvent, compiledJsonPath ){ | |
var emitMatch = oboeBus(fullEventName).emit; | |
predicateEvent.on( function (ascent) { | |
var maybeMatchingMapping = compiledJsonPath(ascent); | |
/* Possible values for maybeMatchingMapping are now: | |
false: | |
we did not match | |
an object/array/string/number/null: | |
we matched and have the node that matched. | |
Because nulls are valid json values this can be null. | |
undefined: | |
we matched but don't have the matching node yet. | |
ie, we know there is an upcoming node that matches but we | |
can't say anything else about it. | |
*/ | |
if (maybeMatchingMapping !== false) { | |
emitMatchingNode( | |
emitMatch, | |
nodeOf(maybeMatchingMapping), | |
ascent | |
); | |
} | |
}, fullEventName); | |
oboeBus('removeListener').on( function(removedEventName){ | |
// if the fully qualified match event listener is later removed, clean up | |
// by removing the underlying listener if it was the last using that pattern: | |
if( removedEventName == fullEventName ) { | |
if( !oboeBus(removedEventName).listeners( )) { | |
predicateEvent.un( fullEventName ); | |
} | |
} | |
}); | |
} | |
oboeBus('newListener').on( function(fullEventName){ | |
var match = /(node|path):(.*)/.exec(fullEventName); | |
if( match ) { | |
var predicateEvent = predicateEventMap[match[1]]; | |
if( !predicateEvent.hasListener( fullEventName) ) { | |
addUnderlyingListener( | |
fullEventName, | |
predicateEvent, | |
jsonPathCompiler( match[2] ) | |
); | |
} | |
} | |
}) | |
} | |
/** | |
* The instance API is the thing that is returned when oboe() is called. | |
* it allows: | |
* | |
* - listeners for various events to be added and removed | |
* - the http response header/headers to be read | |
*/ | |
function instanceApi(oboeBus){ | |
var oboeApi, | |
fullyQualifiedNamePattern = /^(node|path):./, | |
rootNodeFinishedEvent = oboeBus('node:!'), | |
/** | |
* Add any kind of listener that the instance api exposes | |
*/ | |
addListener = varArgs(function( eventId, parameters ){ | |
if( oboeApi[eventId] ) { | |
// for events added as .on(event, callback), if there is a | |
// .event() equivalent with special behaviour , pass through | |
// to that: | |
apply(parameters, oboeApi[eventId]); | |
} else { | |
// we have a standard Node.js EventEmitter 2-argument call. | |
// The first parameter is the listener. | |
var event = oboeBus(eventId), | |
listener = parameters[0]; | |
if( fullyQualifiedNamePattern.test(eventId) ) { | |
// allow fully-qualified node/path listeners | |
// to be added | |
addForgettableCallback(event, listener); | |
} else { | |
// the event has no special handling, pass through | |
// directly onto the event bus: | |
event.on( listener); | |
} | |
} | |
return oboeApi; // chaining | |
}), | |
/** | |
* Remove any kind of listener that the instance api exposes | |
*/ | |
removeListener = function( eventId, p2, p3 ){ | |
if( eventId == 'done' ) { | |
rootNodeFinishedEvent.un(p2); | |
} else if( eventId == 'node' || eventId == 'path' ) { | |
// allow removal of node and path | |
oboeBus.un(eventId + ':' + p2, p3); | |
} else { | |
// we have a standard Node.js EventEmitter 2-argument call. | |
// The second parameter is the listener. This may be a call | |
// to remove a fully-qualified node/path listener but requires | |
// no special handling | |
var listener = p2; | |
oboeBus(eventId).un(listener); | |
} | |
return oboeApi; // chaining | |
}; | |
/** | |
* Add a callback, wrapped in a try/catch so as to not break the | |
* execution of Oboe if an exception is thrown (fail events are | |
* fired instead) | |
* | |
* The callback is used as the listener id so that it can later be | |
* removed using .un(callback) | |
*/ | |
function addProtectedCallback(eventName, callback) { | |
oboeBus(eventName).on(protectedCallback(callback), callback); | |
return oboeApi; // chaining | |
} | |
/** | |
* Add a callback where, if .forget() is called during the callback's | |
* execution, the callback will be de-registered | |
*/ | |
function addForgettableCallback(event, callback) { | |
var safeCallback = protectedCallback(callback); | |
event.on( function() { | |
var discard = false; | |
oboeApi.forget = function(){ | |
discard = true; | |
}; | |
apply( arguments, safeCallback ); | |
delete oboeApi.forget; | |
if( discard ) { | |
event.un(callback); | |
} | |
}, callback) | |
return oboeApi; // chaining | |
} | |
function protectedCallback( callback ) { | |
return function() { | |
try{ | |
callback.apply(oboeApi, arguments); | |
}catch(e) { | |
// An error occured during the callback, publish it on the event bus | |
oboeBus(FAIL_EVENT).emit( errorReport(undefined, undefined, e)); | |
} | |
} | |
} | |
/** | |
* Return the fully qualified event for when a pattern matches | |
* either a node or a path | |
* | |
* @param type {String} either 'node' or 'path' | |
*/ | |
function fullyQualifiedPatternMatchEvent(type, pattern) { | |
return oboeBus(type + ':' + pattern); | |
} | |
/** | |
* Add several listeners at a time, from a map | |
*/ | |
function addListenersMap(eventId, listenerMap) { | |
for( var pattern in listenerMap ) { | |
addForgettableCallback( | |
fullyQualifiedPatternMatchEvent(eventId, pattern), | |
listenerMap[pattern] | |
); | |
} | |
} | |
/** | |
* implementation behind .onPath() and .onNode() | |
*/ | |
function addNodeOrPathListenerApi( eventId, jsonPathOrListenerMap, callback ){ | |
if( isString(jsonPathOrListenerMap) ) { | |
addForgettableCallback( | |
fullyQualifiedPatternMatchEvent(eventId, jsonPathOrListenerMap), | |
callback | |
); | |
} else { | |
addListenersMap(eventId, jsonPathOrListenerMap); | |
} | |
return oboeApi; // chaining | |
} | |
// some interface methods are only filled in after we recieve | |
// values and are noops before that: | |
oboeBus(ROOT_FOUND).on( function(root) { | |
oboeApi.root = functor(root); | |
}); | |
/** | |
* When content starts make the headers readable through the | |
* instance API | |
*/ | |
oboeBus(HTTP_START).on( function(_statusCode, headers) { | |
oboeApi.header = function(name) { | |
return name ? headers[name] | |
: headers | |
; | |
} | |
}); | |
/** | |
* Construct and return the public API of the Oboe instance to be | |
* returned to the calling application | |
*/ | |
return oboeApi = { | |
on : addListener, | |
addListener : addListener, | |
removeListener : removeListener, | |
emit : oboeBus.emit, | |
node : partialComplete(addNodeOrPathListenerApi, 'node'), | |
path : partialComplete(addNodeOrPathListenerApi, 'path'), | |
done : partialComplete(addForgettableCallback, rootNodeFinishedEvent), | |
start : partialComplete(addProtectedCallback, HTTP_START ), | |
// fail doesn't use protectedCallback because | |
// could lead to non-terminating loops | |
fail : oboeBus(FAIL_EVENT).on, | |
// public api calling abort fires the ABORTING event | |
abort : oboeBus(ABORTING).emit, | |
// initially return nothing for header and root | |
header : noop, | |
root : noop | |
}; | |
} | |
/** | |
* This file implements a light-touch central controller for an instance | |
* of Oboe which provides the methods used for interacting with the instance | |
* from the calling app. | |
*/ | |
function instanceController( oboeBus, | |
clarinetParser, contentBuilderHandlers) { | |
oboeBus(STREAM_DATA).on( clarinetParser.write.bind(clarinetParser)); | |
/* At the end of the http content close the clarinet parser. | |
This will provide an error if the total content provided was not | |
valid json, ie if not all arrays, objects and Strings closed properly */ | |
oboeBus(STREAM_END).on( clarinetParser.close.bind(clarinetParser)); | |
/* If we abort this Oboe's request stop listening to the clarinet parser. | |
This prevents more tokens being found after we abort in the case where | |
we aborted during processing of an already filled buffer. */ | |
oboeBus(ABORTING).on( function() { | |
clarinetListenerAdaptor(clarinetParser, {}); | |
}); | |
clarinetListenerAdaptor(clarinetParser, contentBuilderHandlers); | |
// react to errors by putting them on the event bus | |
clarinetParser.onerror = function(e) { | |
oboeBus(FAIL_EVENT).emit( | |
errorReport(undefined, undefined, e) | |
); | |
// note: don't close clarinet here because if it was not expecting | |
// end of the json it will throw an error | |
}; | |
} | |
/** | |
* This file sits just behind the API which is used to attain a new | |
* Oboe instance. It creates the new components that are required | |
* and introduces them to each other. | |
*/ | |
function wire (httpMethodName, contentSource, body, headers){ | |
var oboeBus = pubSub(); | |
headers = headers ? | |
// Shallow-clone the headers array. This allows it to be | |
// modified without side effects to the caller. We don't | |
// want to change objects that the user passes in. | |
JSON.parse(JSON.stringify(headers)) | |
: {}; | |
if( body ) { | |
if( !isString(body) ) { | |
// If the body is not a string, stringify it. This allows objects to | |
// be given which will be sent as JSON. | |
body = JSON.stringify(body); | |
// Default Content-Type to JSON unless given otherwise. | |
headers['Content-Type'] = headers['Content-Type'] || 'application/json'; | |
} | |
} else { | |
body = null; | |
} | |
// Wire the input stream in if we are given a content source. | |
// This will usually be the case. If not, the instance created | |
// will have to be passed content from an external source. | |
if( contentSource ) { | |
streamingHttp( oboeBus, | |
httpTransport(), | |
httpMethodName, | |
contentSource, | |
body, | |
headers | |
); | |
} | |
instanceController( | |
oboeBus, | |
clarinet.parser(), | |
incrementalContentBuilder(oboeBus) | |
); | |
patternAdapter(oboeBus, jsonPathCompiler); | |
return new instanceApi(oboeBus); | |
} | |
// export public API | |
function oboe(arg1, arg2) { | |
if( arg1 ) { | |
if (arg1.url) { | |
// method signature is: | |
// oboe({method:m, url:u, body:b, headers:{...}}) | |
return wire( | |
(arg1.method || 'GET'), | |
url(arg1.url, arg1.cached), | |
arg1.body, | |
arg1.headers | |
); | |
} else { | |
// simple version for GETs. Signature is: | |
// oboe( url ) | |
// | |
return wire( | |
'GET', | |
arg1, // url | |
arg2 // body. Deprecated, use {url:u, body:b} instead | |
); | |
} | |
} else { | |
// wire up a no-AJAX Oboe. Will have to have content | |
// fed in externally and using .emit. | |
return wire(); | |
} | |
// support cache busting like jQuery.ajax({cache:false}) | |
function url(baseUrl, cached) { | |
if( cached === false ) { | |
if( baseUrl.indexOf('?') == -1 ) { | |
baseUrl += '?'; | |
} else { | |
baseUrl += '&'; | |
} | |
baseUrl += '_=' + new Date().getTime(); | |
} | |
return baseUrl; | |
} | |
} | |
;if ( typeof define === "function" && define.amd ) {define( "oboe", [], function () { return oboe; } );} else {window.oboe = oboe;}})(window, Object, Array, Error, JSON); |