mirror of
https://github.com/Wan-Video/Wan2.1.git
synced 2025-06-08 08:14:57 +00:00
2545 lines
78 KiB
JavaScript
2545 lines
78 KiB
JavaScript
// 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); |