Pārlūkot izejas kodu

introduced json form

Tobias Simetsreiter 4 gadi atpakaļ
vecāks
revīzija
e8ee5e918d

+ 58 - 40
src/bootstrap_captive/api/__init__.py

@@ -1,35 +1,71 @@
 
-
-def process_json(inp):
-    if "set_wifi" in inp:
-        return set_wifi(inp["set_wifi"])
-    if "disable_portal" in inp:
-        return disable_portal(inp["disable_portal"])
-    if "index_data" in inp:
-        return index_data(inp["index_data"])
-    return {
-        "ok":False,
-        "api_err":"Request Handler not found"
+def get_form(inp):
+    import os
+    import json
+    form_dir = os.path.realpath(os.path.join(__file__,"..","..","form"))
+    print(form_dir)
+    resp = {
+        "ok": True,
+        "js": [],
     }
+    for f in os.listdir(form_dir):
+        try:
+            jsfile = os.path.join(form_dir, f)
+            print(jsfile)
+            if not f[-5:].lower() == ".json":
+                print("Nope")
+                continue
+            with open(jsfile) as fd:
+                js = json.load(fd)
+                js["name"] = f
+                js["ok"] = True;
+                resp["js"].append(js)
+        except Exception as ex:
+            resp["js"].append({
+                "name": f,
+                "ok": False,
+                "err": str(ex),
+            })
+    return resp
+
+def submit_form(inp):
+    import importlib.machinery
+    import os
 
-def index_data(inp):
+    try:
+        base = os.path.basename(inp["name"])
+        b, ext = os.path.splitext(base)
+        modpath = os.path.join(__file__,"..","..","form", b+".py")
+        modpath = os.path.realpath(modpath)
+        mod = importlib.machinery.SourceFileLoader("bootstrap_portal.form."+b, modpath).load_module()
+        return mod.submit_form(inp, {})
+    except Exception as ex:
+        return {
+            "ok": False,
+            "err": str(ex),
+        }
+
+def get_data(inp):
     wifi_file = "/etc/wpa_supplicant/wpa_supplicant.conf"
     ssid = "";
     pw = ""
-    import re
-    with open(wifi_file) as fd:
-        wifi_config = fd.read()
+    try:
+        import re
+        with open(wifi_file) as fd:
+            wifi_config = fd.read()
 
-    reg = re.search(r'ssid="(.*)', wifi_config)
-    if reg:
-        ssid = reg.groups()[0].rstrip().rstrip('"')
-    reg = re.search(r'psk="(.*)', wifi_config)
-    if reg:
-        psk = reg.groups()[0].rstrip().rstrip('"')
+        reg = re.search(r'ssid="(.*)', wifi_config)
+        if reg:
+            ssid = reg.groups()[0].rstrip().rstrip('"')
+        reg = re.search(r'psk="(.*)', wifi_config)
+        if reg:
+            pw = reg.groups()[0].rstrip().rstrip('"')
+    except Exception as ex:
+        print(ex)
     return {
         "ok":True,
         "ssid": ssid,
-        "pw": psk,
+        "pw": pw,
     }
 
 def disable_portal(inp):
@@ -49,21 +85,3 @@ def disable_portal(inp):
         "ok":True,
     }
 
-def set_wifi(inp):
-    import re
-    ssid = inp["ssid"].replace('"','\"')
-    pw = inp["pw"].replace('"','\"')
-    wifi_file = "/etc/wpa_supplicant/wpa_supplicant.conf"
-    with open(wifi_file) as fd:
-        wifi_conf = fd.read()
-
-    wifi_conf = re.sub(r'ssid=".*','ssid="'+ssid+'"', wifi_conf)
-    wifi_conf = re.sub(r'psk=".*','psk="'+pw+'"', wifi_conf)
-
-    with open(wifi_file, "w") as fd:
-        fd.write(wifi_conf)
-
-    return {
-        "ok":True,
-        "wifi_conf": wifi_conf
-    }

+ 18 - 0
src/bootstrap_captive/api_handler.py

@@ -0,0 +1,18 @@
+
+from bootstrap_captive import api
+
+def process_json(inp):
+    for k in inp:
+        handler = None
+        try:
+            handler = getattr(api, k)
+        except Exception as ex:
+            print(handler, ex)
+        if not handler:
+            continue
+        return handler(inp[k])
+    return {
+        "ok":False,
+        "api_err":"Request Handler not found"
+    }
+

+ 38 - 0
src/bootstrap_captive/form/services.json

@@ -0,0 +1,38 @@
+{
+    "schema": {
+        "service": {
+            "type": "string",
+            "title": "Service",
+            "required": true,
+            "enum": [
+                "ssh",
+                "hostapd",
+                "dhcpcd",
+                "networking"
+            ]
+        },
+        "action": {
+            "type": "string",
+            "title": "Action",
+            "required": true,
+            "enum": [
+                "enable",
+                "disable",
+                "start",
+                "stop"
+            ]
+        }
+    },
+    "form": [
+        {
+            "key": "service",
+            "type": "radiobuttons",
+            "activeClass": "btn-success"
+        },
+        {
+            "key": "action",
+            "type": "radiobuttons",
+            "activeClass": "btn-success"
+        }
+    ]
+}

+ 13 - 0
src/bootstrap_captive/form/services.py

@@ -0,0 +1,13 @@
+
+def submit_form(inp, form):
+    resp = {
+        "ok": True,
+    }
+    try:
+        print("WOLOLO")
+    except Exception as ex:
+        resp["err"].append(str(ex))
+
+    return resp
+
+

+ 24 - 0
src/bootstrap_captive/form/wifi.json

@@ -0,0 +1,24 @@
+{
+  "schema": {
+    "wifi_config": {
+      "type": "object",
+      "title": "Wifi",
+      "properties":{
+          "ssid":{
+              "type": "string",
+              "title": "SSID"
+          },
+          "password":{
+              "type": "string",
+              "title": "password",
+              "type": "password"
+          }
+      }
+    }
+  },
+  "form": [
+    {
+      "key": "wifi_config"
+    }
+  ]
+}

+ 30 - 0
src/bootstrap_captive/form/wifi.py

@@ -0,0 +1,30 @@
+
+def submit_form(inp, form):
+    import re
+    resp = {
+        "ok":True,
+    }
+    inp_conf = inp["wifi_config"]
+    try:
+        ssid = inp_conf["ssid"].replace('"','\"')
+        pw = inp_conf["password"].replace('"','\"')
+        wifi_file = "/etc/wpa_supplicant/wpa_supplicant.conf"
+        with open(wifi_file) as fd:
+            wifi_conf = fd.read()
+
+        wifi_conf = re.sub(r'ssid=".*','ssid="'+ssid+'"', wifi_conf)
+        wifi_conf = re.sub(r'psk=".*','psk="'+pw+'"', wifi_conf)
+
+        with open(wifi_file, "w") as fd:
+            fd.write(wifi_conf)
+
+        resp["wifi_conf"] = wifi_conf
+
+    except Exception as ex:
+        resp["ok"] = False
+        resp["err"] = "Could not set Wifi: "+str(ex)
+
+    return resp
+
+def get_data(form):
+    pass

+ 6 - 5
src/bootstrap_captive/server.py

@@ -7,11 +7,12 @@ from socketserver import ThreadingMixIn
 def server(args):
     httpd = ThreadingHTTPServer((args.bind, args.port), MyHttpRequestHandler)
 
-    httpd_ssl = ThreadingHTTPServer((args.bind, args.ssl_port), MyHttpRequestHandler)
-    httpd_ssl.socket = ssl.wrap_socket (httpd_ssl.socket, certfile=args.certfile, server_side=True)
+    if args.ssl_port:
+        httpd_ssl = ThreadingHTTPServer((args.bind, args.ssl_port), MyHttpRequestHandler)
+        httpd_ssl.socket = ssl.wrap_socket (httpd_ssl.socket, certfile=args.certfile, server_side=True)
+        Thread(target=httpd_ssl.serve_forever).start()
 
-    Thread(target=httpd.serve_forever).start()
-    httpd_ssl.serve_forever()
+    httpd.serve_forever()
 
 class ThreadingHTTPServer(ThreadingMixIn, http.server.HTTPServer):
     daemon_threads = True
@@ -39,7 +40,7 @@ class MyHttpRequestHandler(http.server.SimpleHTTPRequestHandler):
     def do_POST(self):
         import traceback
         import json
-        from .api import process_json
+        from .api_handler import process_json
         self.send_response(200)
         self.send_header('Content-type', 'application/json')
         self.end_headers()

+ 1 - 1
src/bootstrap_captive@.service

@@ -8,7 +8,7 @@ Type=simple
 Restart=always
 RestartSec=1
 User=root
-ExecStart=/usr/local/bin/bootstrap_captive %i
+ExecStart=/usr/local/bin/bootstrap_captive %i 
 
 [Install]
 WantedBy=multi-user.target

+ 9814 - 0
src/http/deps/jquery.min.js

@@ -0,0 +1,9814 @@
+/*!
+ * jQuery JavaScript Library v2.2.4
+ * http://jquery.com/
+ *
+ * Includes Sizzle.js
+ * http://sizzlejs.com/
+ *
+ * Copyright jQuery Foundation and other contributors
+ * Released under the MIT license
+ * http://jquery.org/license
+ *
+ * Date: 2016-05-20T17:23Z
+ */
+
+(function( global, factory ) {
+
+    if ( typeof module === "object" && typeof module.exports === "object" ) {
+        // For CommonJS and CommonJS-like environments where a proper `window`
+        // is present, execute the factory and get jQuery.
+        // For environments that do not have a `window` with a `document`
+        // (such as Node.js), expose a factory as module.exports.
+        // This accentuates the need for the creation of a real `window`.
+        // e.g. var jQuery = require("jquery")(window);
+        // See ticket #14549 for more info.
+        module.exports = global.document ?
+            factory( global, true ) :
+            function( w ) {
+                if ( !w.document ) {
+                    throw new Error( "jQuery requires a window with a document" );
+                }
+                return factory( w );
+            };
+    } else {
+        factory( global );
+    }
+
+// Pass this if window is not defined yet
+}(typeof window !== "undefined" ? window : this, function( window, noGlobal ) {
+
+// Support: Firefox 18+
+// Can't be in strict mode, several libs including ASP.NET trace
+// the stack via arguments.caller.callee and Firefox dies if
+// you try to trace through "use strict" call chains. (#13335)
+//"use strict";
+var arr = [];
+
+var document = window.document;
+
+var slice = arr.slice;
+
+var concat = arr.concat;
+
+var push = arr.push;
+
+var indexOf = arr.indexOf;
+
+var class2type = {};
+
+var toString = class2type.toString;
+
+var hasOwn = class2type.hasOwnProperty;
+
+var support = {};
+
+
+
+var
+    version = "2.2.4",
+
+    // Define a local copy of jQuery
+    jQuery = function( selector, context ) {
+
+        // The jQuery object is actually just the init constructor 'enhanced'
+        // Need init if jQuery is called (just allow error to be thrown if not included)
+        return new jQuery.fn.init( selector, context );
+    },
+
+    // Support: Android<4.1
+    // Make sure we trim BOM and NBSP
+    rtrim = /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,
+
+    // Matches dashed string for camelizing
+    rmsPrefix = /^-ms-/,
+    rdashAlpha = /-([\da-z])/gi,
+
+    // Used by jQuery.camelCase as callback to replace()
+    fcamelCase = function( all, letter ) {
+        return letter.toUpperCase();
+    };
+
+jQuery.fn = jQuery.prototype = {
+
+    // The current version of jQuery being used
+    jquery: version,
+
+    constructor: jQuery,
+
+    // Start with an empty selector
+    selector: "",
+
+    // The default length of a jQuery object is 0
+    length: 0,
+
+    toArray: function() {
+        return slice.call( this );
+    },
+
+    // Get the Nth element in the matched element set OR
+    // Get the whole matched element set as a clean array
+    get: function( num ) {
+        return num != null ?
+
+            // Return just the one element from the set
+            ( num < 0 ? this[ num + this.length ] : this[ num ] ) :
+
+            // Return all the elements in a clean array
+            slice.call( this );
+    },
+
+    // Take an array of elements and push it onto the stack
+    // (returning the new matched element set)
+    pushStack: function( elems ) {
+
+        // Build a new jQuery matched element set
+        var ret = jQuery.merge( this.constructor(), elems );
+
+        // Add the old object onto the stack (as a reference)
+        ret.prevObject = this;
+        ret.context = this.context;
+
+        // Return the newly-formed element set
+        return ret;
+    },
+
+    // Execute a callback for every element in the matched set.
+    each: function( callback ) {
+        return jQuery.each( this, callback );
+    },
+
+    map: function( callback ) {
+        return this.pushStack( jQuery.map( this, function( elem, i ) {
+            return callback.call( elem, i, elem );
+        } ) );
+    },
+
+    slice: function() {
+        return this.pushStack( slice.apply( this, arguments ) );
+    },
+
+    first: function() {
+        return this.eq( 0 );
+    },
+
+    last: function() {
+        return this.eq( -1 );
+    },
+
+    eq: function( i ) {
+        var len = this.length,
+            j = +i + ( i < 0 ? len : 0 );
+        return this.pushStack( j >= 0 && j < len ? [ this[ j ] ] : [] );
+    },
+
+    end: function() {
+        return this.prevObject || this.constructor();
+    },
+
+    // For internal use only.
+    // Behaves like an Array's method, not like a jQuery method.
+    push: push,
+    sort: arr.sort,
+    splice: arr.splice
+};
+
+jQuery.extend = jQuery.fn.extend = function() {
+    var options, name, src, copy, copyIsArray, clone,
+        target = arguments[ 0 ] || {},
+        i = 1,
+        length = arguments.length,
+        deep = false;
+
+    // Handle a deep copy situation
+    if ( typeof target === "boolean" ) {
+        deep = target;
+
+        // Skip the boolean and the target
+        target = arguments[ i ] || {};
+        i++;
+    }
+
+    // Handle case when target is a string or something (possible in deep copy)
+    if ( typeof target !== "object" && !jQuery.isFunction( target ) ) {
+        target = {};
+    }
+
+    // Extend jQuery itself if only one argument is passed
+    if ( i === length ) {
+        target = this;
+        i--;
+    }
+
+    for ( ; i < length; i++ ) {
+
+        // Only deal with non-null/undefined values
+        if ( ( options = arguments[ i ] ) != null ) {
+
+            // Extend the base object
+            for ( name in options ) {
+                src = target[ name ];
+                copy = options[ name ];
+
+                // Prevent never-ending loop
+                if ( target === copy ) {
+                    continue;
+                }
+
+                // Recurse if we're merging plain objects or arrays
+                if ( deep && copy && ( jQuery.isPlainObject( copy ) ||
+                    ( copyIsArray = jQuery.isArray( copy ) ) ) ) {
+
+                    if ( copyIsArray ) {
+                        copyIsArray = false;
+                        clone = src && jQuery.isArray( src ) ? src : [];
+
+                    } else {
+                        clone = src && jQuery.isPlainObject( src ) ? src : {};
+                    }
+
+                    // Never move original objects, clone them
+                    target[ name ] = jQuery.extend( deep, clone, copy );
+
+                // Don't bring in undefined values
+                } else if ( copy !== undefined ) {
+                    target[ name ] = copy;
+                }
+            }
+        }
+    }
+
+    // Return the modified object
+    return target;
+};
+
+jQuery.extend( {
+
+    // Unique for each copy of jQuery on the page
+    expando: "jQuery" + ( version + Math.random() ).replace( /\D/g, "" ),
+
+    // Assume jQuery is ready without the ready module
+    isReady: true,
+
+    error: function( msg ) {
+        throw new Error( msg );
+    },
+
+    noop: function() {},
+
+    isFunction: function( obj ) {
+        return jQuery.type( obj ) === "function";
+    },
+
+    isArray: Array.isArray,
+
+    isWindow: function( obj ) {
+        return obj != null && obj === obj.window;
+    },
+
+    isNumeric: function( obj ) {
+
+        // parseFloat NaNs numeric-cast false positives (null|true|false|"")
+        // ...but misinterprets leading-number strings, particularly hex literals ("0x...")
+        // subtraction forces infinities to NaN
+        // adding 1 corrects loss of precision from parseFloat (#15100)
+        var realStringObj = obj && obj.toString();
+        return !jQuery.isArray( obj ) && ( realStringObj - parseFloat( realStringObj ) + 1 ) >= 0;
+    },
+
+    isPlainObject: function( obj ) {
+        var key;
+
+        // Not plain objects:
+        // - Any object or value whose internal [[Class]] property is not "[object Object]"
+        // - DOM nodes
+        // - window
+        if ( jQuery.type( obj ) !== "object" || obj.nodeType || jQuery.isWindow( obj ) ) {
+            return false;
+        }
+
+        // Not own constructor property must be Object
+        if ( obj.constructor &&
+                !hasOwn.call( obj, "constructor" ) &&
+                !hasOwn.call( obj.constructor.prototype || {}, "isPrototypeOf" ) ) {
+            return false;
+        }
+
+        // Own properties are enumerated firstly, so to speed up,
+        // if last one is own, then all properties are own
+        for ( key in obj ) {}
+
+        return key === undefined || hasOwn.call( obj, key );
+    },
+
+    isEmptyObject: function( obj ) {
+        var name;
+        for ( name in obj ) {
+            return false;
+        }
+        return true;
+    },
+
+    type: function( obj ) {
+        if ( obj == null ) {
+            return obj + "";
+        }
+
+        // Support: Android<4.0, iOS<6 (functionish RegExp)
+        return typeof obj === "object" || typeof obj === "function" ?
+            class2type[ toString.call( obj ) ] || "object" :
+            typeof obj;
+    },
+
+    // Evaluates a script in a global context
+    globalEval: function( code ) {
+        var script,
+            indirect = eval;
+
+        code = jQuery.trim( code );
+
+        if ( code ) {
+
+            // If the code includes a valid, prologue position
+            // strict mode pragma, execute code by injecting a
+            // script tag into the document.
+            if ( code.indexOf( "use strict" ) === 1 ) {
+                script = document.createElement( "script" );
+                script.text = code;
+                document.head.appendChild( script ).parentNode.removeChild( script );
+            } else {
+
+                // Otherwise, avoid the DOM node creation, insertion
+                // and removal by using an indirect global eval
+
+                indirect( code );
+            }
+        }
+    },
+
+    // Convert dashed to camelCase; used by the css and data modules
+    // Support: IE9-11+
+    // Microsoft forgot to hump their vendor prefix (#9572)
+    camelCase: function( string ) {
+        return string.replace( rmsPrefix, "ms-" ).replace( rdashAlpha, fcamelCase );
+    },
+
+    nodeName: function( elem, name ) {
+        return elem.nodeName && elem.nodeName.toLowerCase() === name.toLowerCase();
+    },
+
+    each: function( obj, callback ) {
+        var length, i = 0;
+
+        if ( isArrayLike( obj ) ) {
+            length = obj.length;
+            for ( ; i < length; i++ ) {
+                if ( callback.call( obj[ i ], i, obj[ i ] ) === false ) {
+                    break;
+                }
+            }
+        } else {
+            for ( i in obj ) {
+                if ( callback.call( obj[ i ], i, obj[ i ] ) === false ) {
+                    break;
+                }
+            }
+        }
+
+        return obj;
+    },
+
+    // Support: Android<4.1
+    trim: function( text ) {
+        return text == null ?
+            "" :
+            ( text + "" ).replace( rtrim, "" );
+    },
+
+    // results is for internal usage only
+    makeArray: function( arr, results ) {
+        var ret = results || [];
+
+        if ( arr != null ) {
+            if ( isArrayLike( Object( arr ) ) ) {
+                jQuery.merge( ret,
+                    typeof arr === "string" ?
+                    [ arr ] : arr
+                );
+            } else {
+                push.call( ret, arr );
+            }
+        }
+
+        return ret;
+    },
+
+    inArray: function( elem, arr, i ) {
+        return arr == null ? -1 : indexOf.call( arr, elem, i );
+    },
+
+    merge: function( first, second ) {
+        var len = +second.length,
+            j = 0,
+            i = first.length;
+
+        for ( ; j < len; j++ ) {
+            first[ i++ ] = second[ j ];
+        }
+
+        first.length = i;
+
+        return first;
+    },
+
+    grep: function( elems, callback, invert ) {
+        var callbackInverse,
+            matches = [],
+            i = 0,
+            length = elems.length,
+            callbackExpect = !invert;
+
+        // Go through the array, only saving the items
+        // that pass the validator function
+        for ( ; i < length; i++ ) {
+            callbackInverse = !callback( elems[ i ], i );
+            if ( callbackInverse !== callbackExpect ) {
+                matches.push( elems[ i ] );
+            }
+        }
+
+        return matches;
+    },
+
+    // arg is for internal usage only
+    map: function( elems, callback, arg ) {
+        var length, value,
+            i = 0,
+            ret = [];
+
+        // Go through the array, translating each of the items to their new values
+        if ( isArrayLike( elems ) ) {
+            length = elems.length;
+            for ( ; i < length; i++ ) {
+                value = callback( elems[ i ], i, arg );
+
+                if ( value != null ) {
+                    ret.push( value );
+                }
+            }
+
+        // Go through every key on the object,
+        } else {
+            for ( i in elems ) {
+                value = callback( elems[ i ], i, arg );
+
+                if ( value != null ) {
+                    ret.push( value );
+                }
+            }
+        }
+
+        // Flatten any nested arrays
+        return concat.apply( [], ret );
+    },
+
+    // A global GUID counter for objects
+    guid: 1,
+
+    // Bind a function to a context, optionally partially applying any
+    // arguments.
+    proxy: function( fn, context ) {
+        var tmp, args, proxy;
+
+        if ( typeof context === "string" ) {
+            tmp = fn[ context ];
+            context = fn;
+            fn = tmp;
+        }
+
+        // Quick check to determine if target is callable, in the spec
+        // this throws a TypeError, but we will just return undefined.
+        if ( !jQuery.isFunction( fn ) ) {
+            return undefined;
+        }
+
+        // Simulated bind
+        args = slice.call( arguments, 2 );
+        proxy = function() {
+            return fn.apply( context || this, args.concat( slice.call( arguments ) ) );
+        };
+
+        // Set the guid of unique handler to the same of original handler, so it can be removed
+        proxy.guid = fn.guid = fn.guid || jQuery.guid++;
+
+        return proxy;
+    },
+
+    now: Date.now,
+
+    // jQuery.support is not used in Core but other projects attach their
+    // properties to it so it needs to exist.
+    support: support
+} );
+
+// JSHint would error on this code due to the Symbol not being defined in ES5.
+// Defining this global in .jshintrc would create a danger of using the global
+// unguarded in another place, it seems safer to just disable JSHint for these
+// three lines.
+/* jshint ignore: start */
+if ( typeof Symbol === "function" ) {
+    jQuery.fn[ Symbol.iterator ] = arr[ Symbol.iterator ];
+}
+/* jshint ignore: end */
+
+// Populate the class2type map
+jQuery.each( "Boolean Number String Function Array Date RegExp Object Error Symbol".split( " " ),
+function( i, name ) {
+    class2type[ "[object " + name + "]" ] = name.toLowerCase();
+} );
+
+function isArrayLike( obj ) {
+
+    // Support: iOS 8.2 (not reproducible in simulator)
+    // `in` check used to prevent JIT error (gh-2145)
+    // hasOwn isn't used here due to false negatives
+    // regarding Nodelist length in IE
+    var length = !!obj && "length" in obj && obj.length,
+        type = jQuery.type( obj );
+
+    if ( type === "function" || jQuery.isWindow( obj ) ) {
+        return false;
+    }
+
+    return type === "array" || length === 0 ||
+        typeof length === "number" && length > 0 && ( length - 1 ) in obj;
+}
+var Sizzle =
+/*!
+ * Sizzle CSS Selector Engine v2.2.1
+ * http://sizzlejs.com/
+ *
+ * Copyright jQuery Foundation and other contributors
+ * Released under the MIT license
+ * http://jquery.org/license
+ *
+ * Date: 2015-10-17
+ */
+(function( window ) {
+
+var i,
+    support,
+    Expr,
+    getText,
+    isXML,
+    tokenize,
+    compile,
+    select,
+    outermostContext,
+    sortInput,
+    hasDuplicate,
+
+    // Local document vars
+    setDocument,
+    document,
+    docElem,
+    documentIsHTML,
+    rbuggyQSA,
+    rbuggyMatches,
+    matches,
+    contains,
+
+    // Instance-specific data
+    expando = "sizzle" + 1 * new Date(),
+    preferredDoc = window.document,
+    dirruns = 0,
+    done = 0,
+    classCache = createCache(),
+    tokenCache = createCache(),
+    compilerCache = createCache(),
+    sortOrder = function( a, b ) {
+        if ( a === b ) {
+            hasDuplicate = true;
+        }
+        return 0;
+    },
+
+    // General-purpose constants
+    MAX_NEGATIVE = 1 << 31,
+
+    // Instance methods
+    hasOwn = ({}).hasOwnProperty,
+    arr = [],
+    pop = arr.pop,
+    push_native = arr.push,
+    push = arr.push,
+    slice = arr.slice,
+    // Use a stripped-down indexOf as it's faster than native
+    // http://jsperf.com/thor-indexof-vs-for/5
+    indexOf = function( list, elem ) {
+        var i = 0,
+            len = list.length;
+        for ( ; i < len; i++ ) {
+            if ( list[i] === elem ) {
+                return i;
+            }
+        }
+        return -1;
+    },
+
+    booleans = "checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",
+
+    // Regular expressions
+
+    // http://www.w3.org/TR/css3-selectors/#whitespace
+    whitespace = "[\\x20\\t\\r\\n\\f]",
+
+    // http://www.w3.org/TR/CSS21/syndata.html#value-def-identifier
+    identifier = "(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",
+
+    // Attribute selectors: http://www.w3.org/TR/selectors/#attribute-selectors
+    attributes = "\\[" + whitespace + "*(" + identifier + ")(?:" + whitespace +
+        // Operator (capture 2)
+        "*([*^$|!~]?=)" + whitespace +
+        // "Attribute values must be CSS identifiers [capture 5] or strings [capture 3 or capture 4]"
+        "*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|(" + identifier + "))|)" + whitespace +
+        "*\\]",
+
+    pseudos = ":(" + identifier + ")(?:\\((" +
+        // To reduce the number of selectors needing tokenize in the preFilter, prefer arguments:
+        // 1. quoted (capture 3; capture 4 or capture 5)
+        "('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|" +
+        // 2. simple (capture 6)
+        "((?:\\\\.|[^\\\\()[\\]]|" + attributes + ")*)|" +
+        // 3. anything else (capture 2)
+        ".*" +
+        ")\\)|)",
+
+    // Leading and non-escaped trailing whitespace, capturing some non-whitespace characters preceding the latter
+    rwhitespace = new RegExp( whitespace + "+", "g" ),
+    rtrim = new RegExp( "^" + whitespace + "+|((?:^|[^\\\\])(?:\\\\.)*)" + whitespace + "+$", "g" ),
+
+    rcomma = new RegExp( "^" + whitespace + "*," + whitespace + "*" ),
+    rcombinators = new RegExp( "^" + whitespace + "*([>+~]|" + whitespace + ")" + whitespace + "*" ),
+
+    rattributeQuotes = new RegExp( "=" + whitespace + "*([^\\]'\"]*?)" + whitespace + "*\\]", "g" ),
+
+    rpseudo = new RegExp( pseudos ),
+    ridentifier = new RegExp( "^" + identifier + "$" ),
+
+    matchExpr = {
+        "ID": new RegExp( "^#(" + identifier + ")" ),
+        "CLASS": new RegExp( "^\\.(" + identifier + ")" ),
+        "TAG": new RegExp( "^(" + identifier + "|[*])" ),
+        "ATTR": new RegExp( "^" + attributes ),
+        "PSEUDO": new RegExp( "^" + pseudos ),
+        "CHILD": new RegExp( "^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\(" + whitespace +
+            "*(even|odd|(([+-]|)(\\d*)n|)" + whitespace + "*(?:([+-]|)" + whitespace +
+            "*(\\d+)|))" + whitespace + "*\\)|)", "i" ),
+        "bool": new RegExp( "^(?:" + booleans + ")$", "i" ),
+        // For use in libraries implementing .is()
+        // We use this for POS matching in `select`
+        "needsContext": new RegExp( "^" + whitespace + "*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\(" +
+            whitespace + "*((?:-\\d)?\\d*)" + whitespace + "*\\)|)(?=[^-]|$)", "i" )
+    },
+
+    rinputs = /^(?:input|select|textarea|button)$/i,
+    rheader = /^h\d$/i,
+
+    rnative = /^[^{]+\{\s*\[native \w/,
+
+    // Easily-parseable/retrievable ID or TAG or CLASS selectors
+    rquickExpr = /^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,
+
+    rsibling = /[+~]/,
+    rescape = /'|\\/g,
+
+    // CSS escapes http://www.w3.org/TR/CSS21/syndata.html#escaped-characters
+    runescape = new RegExp( "\\\\([\\da-f]{1,6}" + whitespace + "?|(" + whitespace + ")|.)", "ig" ),
+    funescape = function( _, escaped, escapedWhitespace ) {
+        var high = "0x" + escaped - 0x10000;
+        // NaN means non-codepoint
+        // Support: Firefox<24
+        // Workaround erroneous numeric interpretation of +"0x"
+        return high !== high || escapedWhitespace ?
+            escaped :
+            high < 0 ?
+                // BMP codepoint
+                String.fromCharCode( high + 0x10000 ) :
+                // Supplemental Plane codepoint (surrogate pair)
+                String.fromCharCode( high >> 10 | 0xD800, high & 0x3FF | 0xDC00 );
+    },
+
+    // Used for iframes
+    // See setDocument()
+    // Removing the function wrapper causes a "Permission Denied"
+    // error in IE
+    unloadHandler = function() {
+        setDocument();
+    };
+
+// Optimize for push.apply( _, NodeList )
+try {
+    push.apply(
+        (arr = slice.call( preferredDoc.childNodes )),
+        preferredDoc.childNodes
+    );
+    // Support: Android<4.0
+    // Detect silently failing push.apply
+    arr[ preferredDoc.childNodes.length ].nodeType;
+} catch ( e ) {
+    push = { apply: arr.length ?
+
+        // Leverage slice if possible
+        function( target, els ) {
+            push_native.apply( target, slice.call(els) );
+        } :
+
+        // Support: IE<9
+        // Otherwise append directly
+        function( target, els ) {
+            var j = target.length,
+                i = 0;
+            // Can't trust NodeList.length
+            while ( (target[j++] = els[i++]) ) {}
+            target.length = j - 1;
+        }
+    };
+}
+
+function Sizzle( selector, context, results, seed ) {
+    var m, i, elem, nid, nidselect, match, groups, newSelector,
+        newContext = context && context.ownerDocument,
+
+        // nodeType defaults to 9, since context defaults to document
+        nodeType = context ? context.nodeType : 9;
+
+    results = results || [];
+
+    // Return early from calls with invalid selector or context
+    if ( typeof selector !== "string" || !selector ||
+        nodeType !== 1 && nodeType !== 9 && nodeType !== 11 ) {
+
+        return results;
+    }
+
+    // Try to shortcut find operations (as opposed to filters) in HTML documents
+    if ( !seed ) {
+
+        if ( ( context ? context.ownerDocument || context : preferredDoc ) !== document ) {
+            setDocument( context );
+        }
+        context = context || document;
+
+        if ( documentIsHTML ) {
+
+            // If the selector is sufficiently simple, try using a "get*By*" DOM method
+            // (excepting DocumentFragment context, where the methods don't exist)
+            if ( nodeType !== 11 && (match = rquickExpr.exec( selector )) ) {
+
+                // ID selector
+                if ( (m = match[1]) ) {
+
+                    // Document context
+                    if ( nodeType === 9 ) {
+                        if ( (elem = context.getElementById( m )) ) {
+
+                            // Support: IE, Opera, Webkit
+                            // TODO: identify versions
+                            // getElementById can match elements by name instead of ID
+                            if ( elem.id === m ) {
+                                results.push( elem );
+                                return results;
+                            }
+                        } else {
+                            return results;
+                        }
+
+                    // Element context
+                    } else {
+
+                        // Support: IE, Opera, Webkit
+                        // TODO: identify versions
+                        // getElementById can match elements by name instead of ID
+                        if ( newContext && (elem = newContext.getElementById( m )) &&
+                            contains( context, elem ) &&
+                            elem.id === m ) {
+
+                            results.push( elem );
+                            return results;
+                        }
+                    }
+
+                // Type selector
+                } else if ( match[2] ) {
+                    push.apply( results, context.getElementsByTagName( selector ) );
+                    return results;
+
+                // Class selector
+                } else if ( (m = match[3]) && support.getElementsByClassName &&
+                    context.getElementsByClassName ) {
+
+                    push.apply( results, context.getElementsByClassName( m ) );
+                    return results;
+                }
+            }
+
+            // Take advantage of querySelectorAll
+            if ( support.qsa &&
+                !compilerCache[ selector + " " ] &&
+                (!rbuggyQSA || !rbuggyQSA.test( selector )) ) {
+
+                if ( nodeType !== 1 ) {
+                    newContext = context;
+                    newSelector = selector;
+
+                // qSA looks outside Element context, which is not what we want
+                // Thanks to Andrew Dupont for this workaround technique
+                // Support: IE <=8
+                // Exclude object elements
+                } else if ( context.nodeName.toLowerCase() !== "object" ) {
+
+                    // Capture the context ID, setting it first if necessary
+                    if ( (nid = context.getAttribute( "id" )) ) {
+                        nid = nid.replace( rescape, "\\$&" );
+                    } else {
+                        context.setAttribute( "id", (nid = expando) );
+                    }
+
+                    // Prefix every selector in the list
+                    groups = tokenize( selector );
+                    i = groups.length;
+                    nidselect = ridentifier.test( nid ) ? "#" + nid : "[id='" + nid + "']";
+                    while ( i-- ) {
+                        groups[i] = nidselect + " " + toSelector( groups[i] );
+                    }
+                    newSelector = groups.join( "," );
+
+                    // Expand context for sibling selectors
+                    newContext = rsibling.test( selector ) && testContext( context.parentNode ) ||
+                        context;
+                }
+
+                if ( newSelector ) {
+                    try {
+                        push.apply( results,
+                            newContext.querySelectorAll( newSelector )
+                        );
+                        return results;
+                    } catch ( qsaError ) {
+                    } finally {
+                        if ( nid === expando ) {
+                            context.removeAttribute( "id" );
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    // All others
+    return select( selector.replace( rtrim, "$1" ), context, results, seed );
+}
+
+/**
+ * Create key-value caches of limited size
+ * @returns {function(string, object)} Returns the Object data after storing it on itself with
+ *  property name the (space-suffixed) string and (if the cache is larger than Expr.cacheLength)
+ *  deleting the oldest entry
+ */
+function createCache() {
+    var keys = [];
+
+    function cache( key, value ) {
+        // Use (key + " ") to avoid collision with native prototype properties (see Issue #157)
+        if ( keys.push( key + " " ) > Expr.cacheLength ) {
+            // Only keep the most recent entries
+            delete cache[ keys.shift() ];
+        }
+        return (cache[ key + " " ] = value);
+    }
+    return cache;
+}
+
+/**
+ * Mark a function for special use by Sizzle
+ * @param {Function} fn The function to mark
+ */
+function markFunction( fn ) {
+    fn[ expando ] = true;
+    return fn;
+}
+
+/**
+ * Support testing using an element
+ * @param {Function} fn Passed the created div and expects a boolean result
+ */
+function assert( fn ) {
+    var div = document.createElement("div");
+
+    try {
+        return !!fn( div );
+    } catch (e) {
+        return false;
+    } finally {
+        // Remove from its parent by default
+        if ( div.parentNode ) {
+            div.parentNode.removeChild( div );
+        }
+        // release memory in IE
+        div = null;
+    }
+}
+
+/**
+ * Adds the same handler for all of the specified attrs
+ * @param {String} attrs Pipe-separated list of attributes
+ * @param {Function} handler The method that will be applied
+ */
+function addHandle( attrs, handler ) {
+    var arr = attrs.split("|"),
+        i = arr.length;
+
+    while ( i-- ) {
+        Expr.attrHandle[ arr[i] ] = handler;
+    }
+}
+
+/**
+ * Checks document order of two siblings
+ * @param {Element} a
+ * @param {Element} b
+ * @returns {Number} Returns less than 0 if a precedes b, greater than 0 if a follows b
+ */
+function siblingCheck( a, b ) {
+    var cur = b && a,
+        diff = cur && a.nodeType === 1 && b.nodeType === 1 &&
+            ( ~b.sourceIndex || MAX_NEGATIVE ) -
+            ( ~a.sourceIndex || MAX_NEGATIVE );
+
+    // Use IE sourceIndex if available on both nodes
+    if ( diff ) {
+        return diff;
+    }
+
+    // Check if b follows a
+    if ( cur ) {
+        while ( (cur = cur.nextSibling) ) {
+            if ( cur === b ) {
+                return -1;
+            }
+        }
+    }
+
+    return a ? 1 : -1;
+}
+
+/**
+ * Returns a function to use in pseudos for input types
+ * @param {String} type
+ */
+function createInputPseudo( type ) {
+    return function( elem ) {
+        var name = elem.nodeName.toLowerCase();
+        return name === "input" && elem.type === type;
+    };
+}
+
+/**
+ * Returns a function to use in pseudos for buttons
+ * @param {String} type
+ */
+function createButtonPseudo( type ) {
+    return function( elem ) {
+        var name = elem.nodeName.toLowerCase();
+        return (name === "input" || name === "button") && elem.type === type;
+    };
+}
+
+/**
+ * Returns a function to use in pseudos for positionals
+ * @param {Function} fn
+ */
+function createPositionalPseudo( fn ) {
+    return markFunction(function( argument ) {
+        argument = +argument;
+        return markFunction(function( seed, matches ) {
+            var j,
+                matchIndexes = fn( [], seed.length, argument ),
+                i = matchIndexes.length;
+
+            // Match elements found at the specified indexes
+            while ( i-- ) {
+                if ( seed[ (j = matchIndexes[i]) ] ) {
+                    seed[j] = !(matches[j] = seed[j]);
+                }
+            }
+        });
+    });
+}
+
+/**
+ * Checks a node for validity as a Sizzle context
+ * @param {Element|Object=} context
+ * @returns {Element|Object|Boolean} The input node if acceptable, otherwise a falsy value
+ */
+function testContext( context ) {
+    return context && typeof context.getElementsByTagName !== "undefined" && context;
+}
+
+// Expose support vars for convenience
+support = Sizzle.support = {};
+
+/**
+ * Detects XML nodes
+ * @param {Element|Object} elem An element or a document
+ * @returns {Boolean} True iff elem is a non-HTML XML node
+ */
+isXML = Sizzle.isXML = function( elem ) {
+    // documentElement is verified for cases where it doesn't yet exist
+    // (such as loading iframes in IE - #4833)
+    var documentElement = elem && (elem.ownerDocument || elem).documentElement;
+    return documentElement ? documentElement.nodeName !== "HTML" : false;
+};
+
+/**
+ * Sets document-related variables once based on the current document
+ * @param {Element|Object} [doc] An element or document object to use to set the document
+ * @returns {Object} Returns the current document
+ */
+setDocument = Sizzle.setDocument = function( node ) {
+    var hasCompare, parent,
+        doc = node ? node.ownerDocument || node : preferredDoc;
+
+    // Return early if doc is invalid or already selected
+    if ( doc === document || doc.nodeType !== 9 || !doc.documentElement ) {
+        return document;
+    }
+
+    // Update global variables
+    document = doc;
+    docElem = document.documentElement;
+    documentIsHTML = !isXML( document );
+
+    // Support: IE 9-11, Edge
+    // Accessing iframe documents after unload throws "permission denied" errors (jQuery #13936)
+    if ( (parent = document.defaultView) && parent.top !== parent ) {
+        // Support: IE 11
+        if ( parent.addEventListener ) {
+            parent.addEventListener( "unload", unloadHandler, false );
+
+        // Support: IE 9 - 10 only
+        } else if ( parent.attachEvent ) {
+            parent.attachEvent( "onunload", unloadHandler );
+        }
+    }
+
+    /* Attributes
+    ---------------------------------------------------------------------- */
+
+    // Support: IE<8
+    // Verify that getAttribute really returns attributes and not properties
+    // (excepting IE8 booleans)
+    support.attributes = assert(function( div ) {
+        div.className = "i";
+        return !div.getAttribute("className");
+    });
+
+    /* getElement(s)By*
+    ---------------------------------------------------------------------- */
+
+    // Check if getElementsByTagName("*") returns only elements
+    support.getElementsByTagName = assert(function( div ) {
+        div.appendChild( document.createComment("") );
+        return !div.getElementsByTagName("*").length;
+    });
+
+    // Support: IE<9
+    support.getElementsByClassName = rnative.test( document.getElementsByClassName );
+
+    // Support: IE<10
+    // Check if getElementById returns elements by name
+    // The broken getElementById methods don't pick up programatically-set names,
+    // so use a roundabout getElementsByName test
+    support.getById = assert(function( div ) {
+        docElem.appendChild( div ).id = expando;
+        return !document.getElementsByName || !document.getElementsByName( expando ).length;
+    });
+
+    // ID find and filter
+    if ( support.getById ) {
+        Expr.find["ID"] = function( id, context ) {
+            if ( typeof context.getElementById !== "undefined" && documentIsHTML ) {
+                var m = context.getElementById( id );
+                return m ? [ m ] : [];
+            }
+        };
+        Expr.filter["ID"] = function( id ) {
+            var attrId = id.replace( runescape, funescape );
+            return function( elem ) {
+                return elem.getAttribute("id") === attrId;
+            };
+        };
+    } else {
+        // Support: IE6/7
+        // getElementById is not reliable as a find shortcut
+        delete Expr.find["ID"];
+
+        Expr.filter["ID"] =  function( id ) {
+            var attrId = id.replace( runescape, funescape );
+            return function( elem ) {
+                var node = typeof elem.getAttributeNode !== "undefined" &&
+                    elem.getAttributeNode("id");
+                return node && node.value === attrId;
+            };
+        };
+    }
+
+    // Tag
+    Expr.find["TAG"] = support.getElementsByTagName ?
+        function( tag, context ) {
+            if ( typeof context.getElementsByTagName !== "undefined" ) {
+                return context.getElementsByTagName( tag );
+
+            // DocumentFragment nodes don't have gEBTN
+            } else if ( support.qsa ) {
+                return context.querySelectorAll( tag );
+            }
+        } :
+
+        function( tag, context ) {
+            var elem,
+                tmp = [],
+                i = 0,
+                // By happy coincidence, a (broken) gEBTN appears on DocumentFragment nodes too
+                results = context.getElementsByTagName( tag );
+
+            // Filter out possible comments
+            if ( tag === "*" ) {
+                while ( (elem = results[i++]) ) {
+                    if ( elem.nodeType === 1 ) {
+                        tmp.push( elem );
+                    }
+                }
+
+                return tmp;
+            }
+            return results;
+        };
+
+    // Class
+    Expr.find["CLASS"] = support.getElementsByClassName && function( className, context ) {
+        if ( typeof context.getElementsByClassName !== "undefined" && documentIsHTML ) {
+            return context.getElementsByClassName( className );
+        }
+    };
+
+    /* QSA/matchesSelector
+    ---------------------------------------------------------------------- */
+
+    // QSA and matchesSelector support
+
+    // matchesSelector(:active) reports false when true (IE9/Opera 11.5)
+    rbuggyMatches = [];
+
+    // qSa(:focus) reports false when true (Chrome 21)
+    // We allow this because of a bug in IE8/9 that throws an error
+    // whenever `document.activeElement` is accessed on an iframe
+    // So, we allow :focus to pass through QSA all the time to avoid the IE error
+    // See http://bugs.jquery.com/ticket/13378
+    rbuggyQSA = [];
+
+    if ( (support.qsa = rnative.test( document.querySelectorAll )) ) {
+        // Build QSA regex
+        // Regex strategy adopted from Diego Perini
+        assert(function( div ) {
+            // Select is set to empty string on purpose
+            // This is to test IE's treatment of not explicitly
+            // setting a boolean content attribute,
+            // since its presence should be enough
+            // http://bugs.jquery.com/ticket/12359
+            docElem.appendChild( div ).innerHTML = "<a id='" + expando + "'></a>" +
+                "<select id='" + expando + "-\r\\' msallowcapture=''>" +
+                "<option selected=''></option></select>";
+
+            // Support: IE8, Opera 11-12.16
+            // Nothing should be selected when empty strings follow ^= or $= or *=
+            // The test attribute must be unknown in Opera but "safe" for WinRT
+            // http://msdn.microsoft.com/en-us/library/ie/hh465388.aspx#attribute_section
+            if ( div.querySelectorAll("[msallowcapture^='']").length ) {
+                rbuggyQSA.push( "[*^$]=" + whitespace + "*(?:''|\"\")" );
+            }
+
+            // Support: IE8
+            // Boolean attributes and "value" are not treated correctly
+            if ( !div.querySelectorAll("[selected]").length ) {
+                rbuggyQSA.push( "\\[" + whitespace + "*(?:value|" + booleans + ")" );
+            }
+
+            // Support: Chrome<29, Android<4.4, Safari<7.0+, iOS<7.0+, PhantomJS<1.9.8+
+            if ( !div.querySelectorAll( "[id~=" + expando + "-]" ).length ) {
+                rbuggyQSA.push("~=");
+            }
+
+            // Webkit/Opera - :checked should return selected option elements
+            // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked
+            // IE8 throws error here and will not see later tests
+            if ( !div.querySelectorAll(":checked").length ) {
+                rbuggyQSA.push(":checked");
+            }
+
+            // Support: Safari 8+, iOS 8+
+            // https://bugs.webkit.org/show_bug.cgi?id=136851
+            // In-page `selector#id sibing-combinator selector` fails
+            if ( !div.querySelectorAll( "a#" + expando + "+*" ).length ) {
+                rbuggyQSA.push(".#.+[+~]");
+            }
+        });
+
+        assert(function( div ) {
+            // Support: Windows 8 Native Apps
+            // The type and name attributes are restricted during .innerHTML assignment
+            var input = document.createElement("input");
+            input.setAttribute( "type", "hidden" );
+            div.appendChild( input ).setAttribute( "name", "D" );
+
+            // Support: IE8
+            // Enforce case-sensitivity of name attribute
+            if ( div.querySelectorAll("[name=d]").length ) {
+                rbuggyQSA.push( "name" + whitespace + "*[*^$|!~]?=" );
+            }
+
+            // FF 3.5 - :enabled/:disabled and hidden elements (hidden elements are still enabled)
+            // IE8 throws error here and will not see later tests
+            if ( !div.querySelectorAll(":enabled").length ) {
+                rbuggyQSA.push( ":enabled", ":disabled" );
+            }
+
+            // Opera 10-11 does not throw on post-comma invalid pseudos
+            div.querySelectorAll("*,:x");
+            rbuggyQSA.push(",.*:");
+        });
+    }
+
+    if ( (support.matchesSelector = rnative.test( (matches = docElem.matches ||
+        docElem.webkitMatchesSelector ||
+        docElem.mozMatchesSelector ||
+        docElem.oMatchesSelector ||
+        docElem.msMatchesSelector) )) ) {
+
+        assert(function( div ) {
+            // Check to see if it's possible to do matchesSelector
+            // on a disconnected node (IE 9)
+            support.disconnectedMatch = matches.call( div, "div" );
+
+            // This should fail with an exception
+            // Gecko does not error, returns false instead
+            matches.call( div, "[s!='']:x" );
+            rbuggyMatches.push( "!=", pseudos );
+        });
+    }
+
+    rbuggyQSA = rbuggyQSA.length && new RegExp( rbuggyQSA.join("|") );
+    rbuggyMatches = rbuggyMatches.length && new RegExp( rbuggyMatches.join("|") );
+
+    /* Contains
+    ---------------------------------------------------------------------- */
+    hasCompare = rnative.test( docElem.compareDocumentPosition );
+
+    // Element contains another
+    // Purposefully self-exclusive
+    // As in, an element does not contain itself
+    contains = hasCompare || rnative.test( docElem.contains ) ?
+        function( a, b ) {
+            var adown = a.nodeType === 9 ? a.documentElement : a,
+                bup = b && b.parentNode;
+            return a === bup || !!( bup && bup.nodeType === 1 && (
+                adown.contains ?
+                    adown.contains( bup ) :
+                    a.compareDocumentPosition && a.compareDocumentPosition( bup ) & 16
+            ));
+        } :
+        function( a, b ) {
+            if ( b ) {
+                while ( (b = b.parentNode) ) {
+                    if ( b === a ) {
+                        return true;
+                    }
+                }
+            }
+            return false;
+        };
+
+    /* Sorting
+    ---------------------------------------------------------------------- */
+
+    // Document order sorting
+    sortOrder = hasCompare ?
+    function( a, b ) {
+
+        // Flag for duplicate removal
+        if ( a === b ) {
+            hasDuplicate = true;
+            return 0;
+        }
+
+        // Sort on method existence if only one input has compareDocumentPosition
+        var compare = !a.compareDocumentPosition - !b.compareDocumentPosition;
+        if ( compare ) {
+            return compare;
+        }
+
+        // Calculate position if both inputs belong to the same document
+        compare = ( a.ownerDocument || a ) === ( b.ownerDocument || b ) ?
+            a.compareDocumentPosition( b ) :
+
+            // Otherwise we know they are disconnected
+            1;
+
+        // Disconnected nodes
+        if ( compare & 1 ||
+            (!support.sortDetached && b.compareDocumentPosition( a ) === compare) ) {
+
+            // Choose the first element that is related to our preferred document
+            if ( a === document || a.ownerDocument === preferredDoc && contains(preferredDoc, a) ) {
+                return -1;
+            }
+            if ( b === document || b.ownerDocument === preferredDoc && contains(preferredDoc, b) ) {
+                return 1;
+            }
+
+            // Maintain original order
+            return sortInput ?
+                ( indexOf( sortInput, a ) - indexOf( sortInput, b ) ) :
+                0;
+        }
+
+        return compare & 4 ? -1 : 1;
+    } :
+    function( a, b ) {
+        // Exit early if the nodes are identical
+        if ( a === b ) {
+            hasDuplicate = true;
+            return 0;
+        }
+
+        var cur,
+            i = 0,
+            aup = a.parentNode,
+            bup = b.parentNode,
+            ap = [ a ],
+            bp = [ b ];
+
+        // Parentless nodes are either documents or disconnected
+        if ( !aup || !bup ) {
+            return a === document ? -1 :
+                b === document ? 1 :
+                aup ? -1 :
+                bup ? 1 :
+                sortInput ?
+                ( indexOf( sortInput, a ) - indexOf( sortInput, b ) ) :
+                0;
+
+        // If the nodes are siblings, we can do a quick check
+        } else if ( aup === bup ) {
+            return siblingCheck( a, b );
+        }
+
+        // Otherwise we need full lists of their ancestors for comparison
+        cur = a;
+        while ( (cur = cur.parentNode) ) {
+            ap.unshift( cur );
+        }
+        cur = b;
+        while ( (cur = cur.parentNode) ) {
+            bp.unshift( cur );
+        }
+
+        // Walk down the tree looking for a discrepancy
+        while ( ap[i] === bp[i] ) {
+            i++;
+        }
+
+        return i ?
+            // Do a sibling check if the nodes have a common ancestor
+            siblingCheck( ap[i], bp[i] ) :
+
+            // Otherwise nodes in our document sort first
+            ap[i] === preferredDoc ? -1 :
+            bp[i] === preferredDoc ? 1 :
+            0;
+    };
+
+    return document;
+};
+
+Sizzle.matches = function( expr, elements ) {
+    return Sizzle( expr, null, null, elements );
+};
+
+Sizzle.matchesSelector = function( elem, expr ) {
+    // Set document vars if needed
+    if ( ( elem.ownerDocument || elem ) !== document ) {
+        setDocument( elem );
+    }
+
+    // Make sure that attribute selectors are quoted
+    expr = expr.replace( rattributeQuotes, "='$1']" );
+
+    if ( support.matchesSelector && documentIsHTML &&
+        !compilerCache[ expr + " " ] &&
+        ( !rbuggyMatches || !rbuggyMatches.test( expr ) ) &&
+        ( !rbuggyQSA     || !rbuggyQSA.test( expr ) ) ) {
+
+        try {
+            var ret = matches.call( elem, expr );
+
+            // IE 9's matchesSelector returns false on disconnected nodes
+            if ( ret || support.disconnectedMatch ||
+                    // As well, disconnected nodes are said to be in a document
+                    // fragment in IE 9
+                    elem.document && elem.document.nodeType !== 11 ) {
+                return ret;
+            }
+        } catch (e) {}
+    }
+
+    return Sizzle( expr, document, null, [ elem ] ).length > 0;
+};
+
+Sizzle.contains = function( context, elem ) {
+    // Set document vars if needed
+    if ( ( context.ownerDocument || context ) !== document ) {
+        setDocument( context );
+    }
+    return contains( context, elem );
+};
+
+Sizzle.attr = function( elem, name ) {
+    // Set document vars if needed
+    if ( ( elem.ownerDocument || elem ) !== document ) {
+        setDocument( elem );
+    }
+
+    var fn = Expr.attrHandle[ name.toLowerCase() ],
+        // Don't get fooled by Object.prototype properties (jQuery #13807)
+        val = fn && hasOwn.call( Expr.attrHandle, name.toLowerCase() ) ?
+            fn( elem, name, !documentIsHTML ) :
+            undefined;
+
+    return val !== undefined ?
+        val :
+        support.attributes || !documentIsHTML ?
+            elem.getAttribute( name ) :
+            (val = elem.getAttributeNode(name)) && val.specified ?
+                val.value :
+                null;
+};
+
+Sizzle.error = function( msg ) {
+    throw new Error( "Syntax error, unrecognized expression: " + msg );
+};
+
+/**
+ * Document sorting and removing duplicates
+ * @param {ArrayLike} results
+ */
+Sizzle.uniqueSort = function( results ) {
+    var elem,
+        duplicates = [],
+        j = 0,
+        i = 0;
+
+    // Unless we *know* we can detect duplicates, assume their presence
+    hasDuplicate = !support.detectDuplicates;
+    sortInput = !support.sortStable && results.slice( 0 );
+    results.sort( sortOrder );
+
+    if ( hasDuplicate ) {
+        while ( (elem = results[i++]) ) {
+            if ( elem === results[ i ] ) {
+                j = duplicates.push( i );
+            }
+        }
+        while ( j-- ) {
+            results.splice( duplicates[ j ], 1 );
+        }
+    }
+
+    // Clear input after sorting to release objects
+    // See https://github.com/jquery/sizzle/pull/225
+    sortInput = null;
+
+    return results;
+};
+
+/**
+ * Utility function for retrieving the text value of an array of DOM nodes
+ * @param {Array|Element} elem
+ */
+getText = Sizzle.getText = function( elem ) {
+    var node,
+        ret = "",
+        i = 0,
+        nodeType = elem.nodeType;
+
+    if ( !nodeType ) {
+        // If no nodeType, this is expected to be an array
+        while ( (node = elem[i++]) ) {
+            // Do not traverse comment nodes
+            ret += getText( node );
+        }
+    } else if ( nodeType === 1 || nodeType === 9 || nodeType === 11 ) {
+        // Use textContent for elements
+        // innerText usage removed for consistency of new lines (jQuery #11153)
+        if ( typeof elem.textContent === "string" ) {
+            return elem.textContent;
+        } else {
+            // Traverse its children
+            for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) {
+                ret += getText( elem );
+            }
+        }
+    } else if ( nodeType === 3 || nodeType === 4 ) {
+        return elem.nodeValue;
+    }
+    // Do not include comment or processing instruction nodes
+
+    return ret;
+};
+
+Expr = Sizzle.selectors = {
+
+    // Can be adjusted by the user
+    cacheLength: 50,
+
+    createPseudo: markFunction,
+
+    match: matchExpr,
+
+    attrHandle: {},
+
+    find: {},
+
+    relative: {
+        ">": { dir: "parentNode", first: true },
+        " ": { dir: "parentNode" },
+        "+": { dir: "previousSibling", first: true },
+        "~": { dir: "previousSibling" }
+    },
+
+    preFilter: {
+        "ATTR": function( match ) {
+            match[1] = match[1].replace( runescape, funescape );
+
+            // Move the given value to match[3] whether quoted or unquoted
+            match[3] = ( match[3] || match[4] || match[5] || "" ).replace( runescape, funescape );
+
+            if ( match[2] === "~=" ) {
+                match[3] = " " + match[3] + " ";
+            }
+
+            return match.slice( 0, 4 );
+        },
+
+        "CHILD": function( match ) {
+            /* matches from matchExpr["CHILD"]
+                1 type (only|nth|...)
+                2 what (child|of-type)
+                3 argument (even|odd|\d*|\d*n([+-]\d+)?|...)
+                4 xn-component of xn+y argument ([+-]?\d*n|)
+                5 sign of xn-component
+                6 x of xn-component
+                7 sign of y-component
+                8 y of y-component
+            */
+            match[1] = match[1].toLowerCase();
+
+            if ( match[1].slice( 0, 3 ) === "nth" ) {
+                // nth-* requires argument
+                if ( !match[3] ) {
+                    Sizzle.error( match[0] );
+                }
+
+                // numeric x and y parameters for Expr.filter.CHILD
+                // remember that false/true cast respectively to 0/1
+                match[4] = +( match[4] ? match[5] + (match[6] || 1) : 2 * ( match[3] === "even" || match[3] === "odd" ) );
+                match[5] = +( ( match[7] + match[8] ) || match[3] === "odd" );
+
+            // other types prohibit arguments
+            } else if ( match[3] ) {
+                Sizzle.error( match[0] );
+            }
+
+            return match;
+        },
+
+        "PSEUDO": function( match ) {
+            var excess,
+                unquoted = !match[6] && match[2];
+
+            if ( matchExpr["CHILD"].test( match[0] ) ) {
+                return null;
+            }
+
+            // Accept quoted arguments as-is
+            if ( match[3] ) {
+                match[2] = match[4] || match[5] || "";
+
+            // Strip excess characters from unquoted arguments
+            } else if ( unquoted && rpseudo.test( unquoted ) &&
+                // Get excess from tokenize (recursively)
+                (excess = tokenize( unquoted, true )) &&
+                // advance to the next closing parenthesis
+                (excess = unquoted.indexOf( ")", unquoted.length - excess ) - unquoted.length) ) {
+
+                // excess is a negative index
+                match[0] = match[0].slice( 0, excess );
+                match[2] = unquoted.slice( 0, excess );
+            }
+
+            // Return only captures needed by the pseudo filter method (type and argument)
+            return match.slice( 0, 3 );
+        }
+    },
+
+    filter: {
+
+        "TAG": function( nodeNameSelector ) {
+            var nodeName = nodeNameSelector.replace( runescape, funescape ).toLowerCase();
+            return nodeNameSelector === "*" ?
+                function() { return true; } :
+                function( elem ) {
+                    return elem.nodeName && elem.nodeName.toLowerCase() === nodeName;
+                };
+        },
+
+        "CLASS": function( className ) {
+            var pattern = classCache[ className + " " ];
+
+            return pattern ||
+                (pattern = new RegExp( "(^|" + whitespace + ")" + className + "(" + whitespace + "|$)" )) &&
+                classCache( className, function( elem ) {
+                    return pattern.test( typeof elem.className === "string" && elem.className || typeof elem.getAttribute !== "undefined" && elem.getAttribute("class") || "" );
+                });
+        },
+
+        "ATTR": function( name, operator, check ) {
+            return function( elem ) {
+                var result = Sizzle.attr( elem, name );
+
+                if ( result == null ) {
+                    return operator === "!=";
+                }
+                if ( !operator ) {
+                    return true;
+                }
+
+                result += "";
+
+                return operator === "=" ? result === check :
+                    operator === "!=" ? result !== check :
+                    operator === "^=" ? check && result.indexOf( check ) === 0 :
+                    operator === "*=" ? check && result.indexOf( check ) > -1 :
+                    operator === "$=" ? check && result.slice( -check.length ) === check :
+                    operator === "~=" ? ( " " + result.replace( rwhitespace, " " ) + " " ).indexOf( check ) > -1 :
+                    operator === "|=" ? result === check || result.slice( 0, check.length + 1 ) === check + "-" :
+                    false;
+            };
+        },
+
+        "CHILD": function( type, what, argument, first, last ) {
+            var simple = type.slice( 0, 3 ) !== "nth",
+                forward = type.slice( -4 ) !== "last",
+                ofType = what === "of-type";
+
+            return first === 1 && last === 0 ?
+
+                // Shortcut for :nth-*(n)
+                function( elem ) {
+                    return !!elem.parentNode;
+                } :
+
+                function( elem, context, xml ) {
+                    var cache, uniqueCache, outerCache, node, nodeIndex, start,
+                        dir = simple !== forward ? "nextSibling" : "previousSibling",
+                        parent = elem.parentNode,
+                        name = ofType && elem.nodeName.toLowerCase(),
+                        useCache = !xml && !ofType,
+                        diff = false;
+
+                    if ( parent ) {
+
+                        // :(first|last|only)-(child|of-type)
+                        if ( simple ) {
+                            while ( dir ) {
+                                node = elem;
+                                while ( (node = node[ dir ]) ) {
+                                    if ( ofType ?
+                                        node.nodeName.toLowerCase() === name :
+                                        node.nodeType === 1 ) {
+
+                                        return false;
+                                    }
+                                }
+                                // Reverse direction for :only-* (if we haven't yet done so)
+                                start = dir = type === "only" && !start && "nextSibling";
+                            }
+                            return true;
+                        }
+
+                        start = [ forward ? parent.firstChild : parent.lastChild ];
+
+                        // non-xml :nth-child(...) stores cache data on `parent`
+                        if ( forward && useCache ) {
+
+                            // Seek `elem` from a previously-cached index
+
+                            // ...in a gzip-friendly way
+                            node = parent;
+                            outerCache = node[ expando ] || (node[ expando ] = {});
+
+                            // Support: IE <9 only
+                            // Defend against cloned attroperties (jQuery gh-1709)
+                            uniqueCache = outerCache[ node.uniqueID ] ||
+                                (outerCache[ node.uniqueID ] = {});
+
+                            cache = uniqueCache[ type ] || [];
+                            nodeIndex = cache[ 0 ] === dirruns && cache[ 1 ];
+                            diff = nodeIndex && cache[ 2 ];
+                            node = nodeIndex && parent.childNodes[ nodeIndex ];
+
+                            while ( (node = ++nodeIndex && node && node[ dir ] ||
+
+                                // Fallback to seeking `elem` from the start
+                                (diff = nodeIndex = 0) || start.pop()) ) {
+
+                                // When found, cache indexes on `parent` and break
+                                if ( node.nodeType === 1 && ++diff && node === elem ) {
+                                    uniqueCache[ type ] = [ dirruns, nodeIndex, diff ];
+                                    break;
+                                }
+                            }
+
+                        } else {
+                            // Use previously-cached element index if available
+                            if ( useCache ) {
+                                // ...in a gzip-friendly way
+                                node = elem;
+                                outerCache = node[ expando ] || (node[ expando ] = {});
+
+                                // Support: IE <9 only
+                                // Defend against cloned attroperties (jQuery gh-1709)
+                                uniqueCache = outerCache[ node.uniqueID ] ||
+                                    (outerCache[ node.uniqueID ] = {});
+
+                                cache = uniqueCache[ type ] || [];
+                                nodeIndex = cache[ 0 ] === dirruns && cache[ 1 ];
+                                diff = nodeIndex;
+                            }
+
+                            // xml :nth-child(...)
+                            // or :nth-last-child(...) or :nth(-last)?-of-type(...)
+                            if ( diff === false ) {
+                                // Use the same loop as above to seek `elem` from the start
+                                while ( (node = ++nodeIndex && node && node[ dir ] ||
+                                    (diff = nodeIndex = 0) || start.pop()) ) {
+
+                                    if ( ( ofType ?
+                                        node.nodeName.toLowerCase() === name :
+                                        node.nodeType === 1 ) &&
+                                        ++diff ) {
+
+                                        // Cache the index of each encountered element
+                                        if ( useCache ) {
+                                            outerCache = node[ expando ] || (node[ expando ] = {});
+
+                                            // Support: IE <9 only
+                                            // Defend against cloned attroperties (jQuery gh-1709)
+                                            uniqueCache = outerCache[ node.uniqueID ] ||
+                                                (outerCache[ node.uniqueID ] = {});
+
+                                            uniqueCache[ type ] = [ dirruns, diff ];
+                                        }
+
+                                        if ( node === elem ) {
+                                            break;
+                                        }
+                                    }
+                                }
+                            }
+                        }
+
+                        // Incorporate the offset, then check against cycle size
+                        diff -= last;
+                        return diff === first || ( diff % first === 0 && diff / first >= 0 );
+                    }
+                };
+        },
+
+        "PSEUDO": function( pseudo, argument ) {
+            // pseudo-class names are case-insensitive
+            // http://www.w3.org/TR/selectors/#pseudo-classes
+            // Prioritize by case sensitivity in case custom pseudos are added with uppercase letters
+            // Remember that setFilters inherits from pseudos
+            var args,
+                fn = Expr.pseudos[ pseudo ] || Expr.setFilters[ pseudo.toLowerCase() ] ||
+                    Sizzle.error( "unsupported pseudo: " + pseudo );
+
+            // The user may use createPseudo to indicate that
+            // arguments are needed to create the filter function
+            // just as Sizzle does
+            if ( fn[ expando ] ) {
+                return fn( argument );
+            }
+
+            // But maintain support for old signatures
+            if ( fn.length > 1 ) {
+                args = [ pseudo, pseudo, "", argument ];
+                return Expr.setFilters.hasOwnProperty( pseudo.toLowerCase() ) ?
+                    markFunction(function( seed, matches ) {
+                        var idx,
+                            matched = fn( seed, argument ),
+                            i = matched.length;
+                        while ( i-- ) {
+                            idx = indexOf( seed, matched[i] );
+                            seed[ idx ] = !( matches[ idx ] = matched[i] );
+                        }
+                    }) :
+                    function( elem ) {
+                        return fn( elem, 0, args );
+                    };
+            }
+
+            return fn;
+        }
+    },
+
+    pseudos: {
+        // Potentially complex pseudos
+        "not": markFunction(function( selector ) {
+            // Trim the selector passed to compile
+            // to avoid treating leading and trailing
+            // spaces as combinators
+            var input = [],
+                results = [],
+                matcher = compile( selector.replace( rtrim, "$1" ) );
+
+            return matcher[ expando ] ?
+                markFunction(function( seed, matches, context, xml ) {
+                    var elem,
+                        unmatched = matcher( seed, null, xml, [] ),
+                        i = seed.length;
+
+                    // Match elements unmatched by `matcher`
+                    while ( i-- ) {
+                        if ( (elem = unmatched[i]) ) {
+                            seed[i] = !(matches[i] = elem);
+                        }
+                    }
+                }) :
+                function( elem, context, xml ) {
+                    input[0] = elem;
+                    matcher( input, null, xml, results );
+                    // Don't keep the element (issue #299)
+                    input[0] = null;
+                    return !results.pop();
+                };
+        }),
+
+        "has": markFunction(function( selector ) {
+            return function( elem ) {
+                return Sizzle( selector, elem ).length > 0;
+            };
+        }),
+
+        "contains": markFunction(function( text ) {
+            text = text.replace( runescape, funescape );
+            return function( elem ) {
+                return ( elem.textContent || elem.innerText || getText( elem ) ).indexOf( text ) > -1;
+            };
+        }),
+
+        // "Whether an element is represented by a :lang() selector
+        // is based solely on the element's language value
+        // being equal to the identifier C,
+        // or beginning with the identifier C immediately followed by "-".
+        // The matching of C against the element's language value is performed case-insensitively.
+        // The identifier C does not have to be a valid language name."
+        // http://www.w3.org/TR/selectors/#lang-pseudo
+        "lang": markFunction( function( lang ) {
+            // lang value must be a valid identifier
+            if ( !ridentifier.test(lang || "") ) {
+                Sizzle.error( "unsupported lang: " + lang );
+            }
+            lang = lang.replace( runescape, funescape ).toLowerCase();
+            return function( elem ) {
+                var elemLang;
+                do {
+                    if ( (elemLang = documentIsHTML ?
+                        elem.lang :
+                        elem.getAttribute("xml:lang") || elem.getAttribute("lang")) ) {
+
+                        elemLang = elemLang.toLowerCase();
+                        return elemLang === lang || elemLang.indexOf( lang + "-" ) === 0;
+                    }
+                } while ( (elem = elem.parentNode) && elem.nodeType === 1 );
+                return false;
+            };
+        }),
+
+        // Miscellaneous
+        "target": function( elem ) {
+            var hash = window.location && window.location.hash;
+            return hash && hash.slice( 1 ) === elem.id;
+        },
+
+        "root": function( elem ) {
+            return elem === docElem;
+        },
+
+        "focus": function( elem ) {
+            return elem === document.activeElement && (!document.hasFocus || document.hasFocus()) && !!(elem.type || elem.href || ~elem.tabIndex);
+        },
+
+        // Boolean properties
+        "enabled": function( elem ) {
+            return elem.disabled === false;
+        },
+
+        "disabled": function( elem ) {
+            return elem.disabled === true;
+        },
+
+        "checked": function( elem ) {
+            // In CSS3, :checked should return both checked and selected elements
+            // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked
+            var nodeName = elem.nodeName.toLowerCase();
+            return (nodeName === "input" && !!elem.checked) || (nodeName === "option" && !!elem.selected);
+        },
+
+        "selected": function( elem ) {
+            // Accessing this property makes selected-by-default
+            // options in Safari work properly
+            if ( elem.parentNode ) {
+                elem.parentNode.selectedIndex;
+            }
+
+            return elem.selected === true;
+        },
+
+        // Contents
+        "empty": function( elem ) {
+            // http://www.w3.org/TR/selectors/#empty-pseudo
+            // :empty is negated by element (1) or content nodes (text: 3; cdata: 4; entity ref: 5),
+            //   but not by others (comment: 8; processing instruction: 7; etc.)
+            // nodeType < 6 works because attributes (2) do not appear as children
+            for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) {
+                if ( elem.nodeType < 6 ) {
+                    return false;
+                }
+            }
+            return true;
+        },
+
+        "parent": function( elem ) {
+            return !Expr.pseudos["empty"]( elem );
+        },
+
+        // Element/input types
+        "header": function( elem ) {
+            return rheader.test( elem.nodeName );
+        },
+
+        "input": function( elem ) {
+            return rinputs.test( elem.nodeName );
+        },
+
+        "button": function( elem ) {
+            var name = elem.nodeName.toLowerCase();
+            return name === "input" && elem.type === "button" || name === "button";
+        },
+
+        "text": function( elem ) {
+            var attr;
+            return elem.nodeName.toLowerCase() === "input" &&
+                elem.type === "text" &&
+
+                // Support: IE<8
+                // New HTML5 attribute values (e.g., "search") appear with elem.type === "text"
+                ( (attr = elem.getAttribute("type")) == null || attr.toLowerCase() === "text" );
+        },
+
+        // Position-in-collection
+        "first": createPositionalPseudo(function() {
+            return [ 0 ];
+        }),
+
+        "last": createPositionalPseudo(function( matchIndexes, length ) {
+            return [ length - 1 ];
+        }),
+
+        "eq": createPositionalPseudo(function( matchIndexes, length, argument ) {
+            return [ argument < 0 ? argument + length : argument ];
+        }),
+
+        "even": createPositionalPseudo(function( matchIndexes, length ) {
+            var i = 0;
+            for ( ; i < length; i += 2 ) {
+                matchIndexes.push( i );
+            }
+            return matchIndexes;
+        }),
+
+        "odd": createPositionalPseudo(function( matchIndexes, length ) {
+            var i = 1;
+            for ( ; i < length; i += 2 ) {
+                matchIndexes.push( i );
+            }
+            return matchIndexes;
+        }),
+
+        "lt": createPositionalPseudo(function( matchIndexes, length, argument ) {
+            var i = argument < 0 ? argument + length : argument;
+            for ( ; --i >= 0; ) {
+                matchIndexes.push( i );
+            }
+            return matchIndexes;
+        }),
+
+        "gt": createPositionalPseudo(function( matchIndexes, length, argument ) {
+            var i = argument < 0 ? argument + length : argument;
+            for ( ; ++i < length; ) {
+                matchIndexes.push( i );
+            }
+            return matchIndexes;
+        })
+    }
+};
+
+Expr.pseudos["nth"] = Expr.pseudos["eq"];
+
+// Add button/input type pseudos
+for ( i in { radio: true, checkbox: true, file: true, password: true, image: true } ) {
+    Expr.pseudos[ i ] = createInputPseudo( i );
+}
+for ( i in { submit: true, reset: true } ) {
+    Expr.pseudos[ i ] = createButtonPseudo( i );
+}
+
+// Easy API for creating new setFilters
+function setFilters() {}
+setFilters.prototype = Expr.filters = Expr.pseudos;
+Expr.setFilters = new setFilters();
+
+tokenize = Sizzle.tokenize = function( selector, parseOnly ) {
+    var matched, match, tokens, type,
+        soFar, groups, preFilters,
+        cached = tokenCache[ selector + " " ];
+
+    if ( cached ) {
+        return parseOnly ? 0 : cached.slice( 0 );
+    }
+
+    soFar = selector;
+    groups = [];
+    preFilters = Expr.preFilter;
+
+    while ( soFar ) {
+
+        // Comma and first run
+        if ( !matched || (match = rcomma.exec( soFar )) ) {
+            if ( match ) {
+                // Don't consume trailing commas as valid
+                soFar = soFar.slice( match[0].length ) || soFar;
+            }
+            groups.push( (tokens = []) );
+        }
+
+        matched = false;
+
+        // Combinators
+        if ( (match = rcombinators.exec( soFar )) ) {
+            matched = match.shift();
+            tokens.push({
+                value: matched,
+                // Cast descendant combinators to space
+                type: match[0].replace( rtrim, " " )
+            });
+            soFar = soFar.slice( matched.length );
+        }
+
+        // Filters
+        for ( type in Expr.filter ) {
+            if ( (match = matchExpr[ type ].exec( soFar )) && (!preFilters[ type ] ||
+                (match = preFilters[ type ]( match ))) ) {
+                matched = match.shift();
+                tokens.push({
+                    value: matched,
+                    type: type,
+                    matches: match
+                });
+                soFar = soFar.slice( matched.length );
+            }
+        }
+
+        if ( !matched ) {
+            break;
+        }
+    }
+
+    // Return the length of the invalid excess
+    // if we're just parsing
+    // Otherwise, throw an error or return tokens
+    return parseOnly ?
+        soFar.length :
+        soFar ?
+            Sizzle.error( selector ) :
+            // Cache the tokens
+            tokenCache( selector, groups ).slice( 0 );
+};
+
+function toSelector( tokens ) {
+    var i = 0,
+        len = tokens.length,
+        selector = "";
+    for ( ; i < len; i++ ) {
+        selector += tokens[i].value;
+    }
+    return selector;
+}
+
+function addCombinator( matcher, combinator, base ) {
+    var dir = combinator.dir,
+        checkNonElements = base && dir === "parentNode",
+        doneName = done++;
+
+    return combinator.first ?
+        // Check against closest ancestor/preceding element
+        function( elem, context, xml ) {
+            while ( (elem = elem[ dir ]) ) {
+                if ( elem.nodeType === 1 || checkNonElements ) {
+                    return matcher( elem, context, xml );
+                }
+            }
+        } :
+
+        // Check against all ancestor/preceding elements
+        function( elem, context, xml ) {
+            var oldCache, uniqueCache, outerCache,
+                newCache = [ dirruns, doneName ];
+
+            // We can't set arbitrary data on XML nodes, so they don't benefit from combinator caching
+            if ( xml ) {
+                while ( (elem = elem[ dir ]) ) {
+                    if ( elem.nodeType === 1 || checkNonElements ) {
+                        if ( matcher( elem, context, xml ) ) {
+                            return true;
+                        }
+                    }
+                }
+            } else {
+                while ( (elem = elem[ dir ]) ) {
+                    if ( elem.nodeType === 1 || checkNonElements ) {
+                        outerCache = elem[ expando ] || (elem[ expando ] = {});
+
+                        // Support: IE <9 only
+                        // Defend against cloned attroperties (jQuery gh-1709)
+                        uniqueCache = outerCache[ elem.uniqueID ] || (outerCache[ elem.uniqueID ] = {});
+
+                        if ( (oldCache = uniqueCache[ dir ]) &&
+                            oldCache[ 0 ] === dirruns && oldCache[ 1 ] === doneName ) {
+
+                            // Assign to newCache so results back-propagate to previous elements
+                            return (newCache[ 2 ] = oldCache[ 2 ]);
+                        } else {
+                            // Reuse newcache so results back-propagate to previous elements
+                            uniqueCache[ dir ] = newCache;
+
+                            // A match means we're done; a fail means we have to keep checking
+                            if ( (newCache[ 2 ] = matcher( elem, context, xml )) ) {
+                                return true;
+                            }
+                        }
+                    }
+                }
+            }
+        };
+}
+
+function elementMatcher( matchers ) {
+    return matchers.length > 1 ?
+        function( elem, context, xml ) {
+            var i = matchers.length;
+            while ( i-- ) {
+                if ( !matchers[i]( elem, context, xml ) ) {
+                    return false;
+                }
+            }
+            return true;
+        } :
+        matchers[0];
+}
+
+function multipleContexts( selector, contexts, results ) {
+    var i = 0,
+        len = contexts.length;
+    for ( ; i < len; i++ ) {
+        Sizzle( selector, contexts[i], results );
+    }
+    return results;
+}
+
+function condense( unmatched, map, filter, context, xml ) {
+    var elem,
+        newUnmatched = [],
+        i = 0,
+        len = unmatched.length,
+        mapped = map != null;
+
+    for ( ; i < len; i++ ) {
+        if ( (elem = unmatched[i]) ) {
+            if ( !filter || filter( elem, context, xml ) ) {
+                newUnmatched.push( elem );
+                if ( mapped ) {
+                    map.push( i );
+                }
+            }
+        }
+    }
+
+    return newUnmatched;
+}
+
+function setMatcher( preFilter, selector, matcher, postFilter, postFinder, postSelector ) {
+    if ( postFilter && !postFilter[ expando ] ) {
+        postFilter = setMatcher( postFilter );
+    }
+    if ( postFinder && !postFinder[ expando ] ) {
+        postFinder = setMatcher( postFinder, postSelector );
+    }
+    return markFunction(function( seed, results, context, xml ) {
+        var temp, i, elem,
+            preMap = [],
+            postMap = [],
+            preexisting = results.length,
+
+            // Get initial elements from seed or context
+            elems = seed || multipleContexts( selector || "*", context.nodeType ? [ context ] : context, [] ),
+
+            // Prefilter to get matcher input, preserving a map for seed-results synchronization
+            matcherIn = preFilter && ( seed || !selector ) ?
+                condense( elems, preMap, preFilter, context, xml ) :
+                elems,
+
+            matcherOut = matcher ?
+                // If we have a postFinder, or filtered seed, or non-seed postFilter or preexisting results,
+                postFinder || ( seed ? preFilter : preexisting || postFilter ) ?
+
+                    // ...intermediate processing is necessary
+                    [] :
+
+                    // ...otherwise use results directly
+                    results :
+                matcherIn;
+
+        // Find primary matches
+        if ( matcher ) {
+            matcher( matcherIn, matcherOut, context, xml );
+        }
+
+        // Apply postFilter
+        if ( postFilter ) {
+            temp = condense( matcherOut, postMap );
+            postFilter( temp, [], context, xml );
+
+            // Un-match failing elements by moving them back to matcherIn
+            i = temp.length;
+            while ( i-- ) {
+                if ( (elem = temp[i]) ) {
+                    matcherOut[ postMap[i] ] = !(matcherIn[ postMap[i] ] = elem);
+                }
+            }
+        }
+
+        if ( seed ) {
+            if ( postFinder || preFilter ) {
+                if ( postFinder ) {
+                    // Get the final matcherOut by condensing this intermediate into postFinder contexts
+                    temp = [];
+                    i = matcherOut.length;
+                    while ( i-- ) {
+                        if ( (elem = matcherOut[i]) ) {
+                            // Restore matcherIn since elem is not yet a final match
+                            temp.push( (matcherIn[i] = elem) );
+                        }
+                    }
+                    postFinder( null, (matcherOut = []), temp, xml );
+                }
+
+                // Move matched elements from seed to results to keep them synchronized
+                i = matcherOut.length;
+                while ( i-- ) {
+                    if ( (elem = matcherOut[i]) &&
+                        (temp = postFinder ? indexOf( seed, elem ) : preMap[i]) > -1 ) {
+
+                        seed[temp] = !(results[temp] = elem);
+                    }
+                }
+            }
+
+        // Add elements to results, through postFinder if defined
+        } else {
+            matcherOut = condense(
+                matcherOut === results ?
+                    matcherOut.splice( preexisting, matcherOut.length ) :
+                    matcherOut
+            );
+            if ( postFinder ) {
+                postFinder( null, results, matcherOut, xml );
+            } else {
+                push.apply( results, matcherOut );
+            }
+        }
+    });
+}
+
+function matcherFromTokens( tokens ) {
+    var checkContext, matcher, j,
+        len = tokens.length,
+        leadingRelative = Expr.relative[ tokens[0].type ],
+        implicitRelative = leadingRelative || Expr.relative[" "],
+        i = leadingRelative ? 1 : 0,
+
+        // The foundational matcher ensures that elements are reachable from top-level context(s)
+        matchContext = addCombinator( function( elem ) {
+            return elem === checkContext;
+        }, implicitRelative, true ),
+        matchAnyContext = addCombinator( function( elem ) {
+            return indexOf( checkContext, elem ) > -1;
+        }, implicitRelative, true ),
+        matchers = [ function( elem, context, xml ) {
+            var ret = ( !leadingRelative && ( xml || context !== outermostContext ) ) || (
+                (checkContext = context).nodeType ?
+                    matchContext( elem, context, xml ) :
+                    matchAnyContext( elem, context, xml ) );
+            // Avoid hanging onto element (issue #299)
+            checkContext = null;
+            return ret;
+        } ];
+
+    for ( ; i < len; i++ ) {
+        if ( (matcher = Expr.relative[ tokens[i].type ]) ) {
+            matchers = [ addCombinator(elementMatcher( matchers ), matcher) ];
+        } else {
+            matcher = Expr.filter[ tokens[i].type ].apply( null, tokens[i].matches );
+
+            // Return special upon seeing a positional matcher
+            if ( matcher[ expando ] ) {
+                // Find the next relative operator (if any) for proper handling
+                j = ++i;
+                for ( ; j < len; j++ ) {
+                    if ( Expr.relative[ tokens[j].type ] ) {
+                        break;
+                    }
+                }
+                return setMatcher(
+                    i > 1 && elementMatcher( matchers ),
+                    i > 1 && toSelector(
+                        // If the preceding token was a descendant combinator, insert an implicit any-element `*`
+                        tokens.slice( 0, i - 1 ).concat({ value: tokens[ i - 2 ].type === " " ? "*" : "" })
+                    ).replace( rtrim, "$1" ),
+                    matcher,
+                    i < j && matcherFromTokens( tokens.slice( i, j ) ),
+                    j < len && matcherFromTokens( (tokens = tokens.slice( j )) ),
+                    j < len && toSelector( tokens )
+                );
+            }
+            matchers.push( matcher );
+        }
+    }
+
+    return elementMatcher( matchers );
+}
+
+function matcherFromGroupMatchers( elementMatchers, setMatchers ) {
+    var bySet = setMatchers.length > 0,
+        byElement = elementMatchers.length > 0,
+        superMatcher = function( seed, context, xml, results, outermost ) {
+            var elem, j, matcher,
+                matchedCount = 0,
+                i = "0",
+                unmatched = seed && [],
+                setMatched = [],
+                contextBackup = outermostContext,
+                // We must always have either seed elements or outermost context
+                elems = seed || byElement && Expr.find["TAG"]( "*", outermost ),
+                // Use integer dirruns iff this is the outermost matcher
+                dirrunsUnique = (dirruns += contextBackup == null ? 1 : Math.random() || 0.1),
+                len = elems.length;
+
+            if ( outermost ) {
+                outermostContext = context === document || context || outermost;
+            }
+
+            // Add elements passing elementMatchers directly to results
+            // Support: IE<9, Safari
+            // Tolerate NodeList properties (IE: "length"; Safari: <number>) matching elements by id
+            for ( ; i !== len && (elem = elems[i]) != null; i++ ) {
+                if ( byElement && elem ) {
+                    j = 0;
+                    if ( !context && elem.ownerDocument !== document ) {
+                        setDocument( elem );
+                        xml = !documentIsHTML;
+                    }
+                    while ( (matcher = elementMatchers[j++]) ) {
+                        if ( matcher( elem, context || document, xml) ) {
+                            results.push( elem );
+                            break;
+                        }
+                    }
+                    if ( outermost ) {
+                        dirruns = dirrunsUnique;
+                    }
+                }
+
+                // Track unmatched elements for set filters
+                if ( bySet ) {
+                    // They will have gone through all possible matchers
+                    if ( (elem = !matcher && elem) ) {
+                        matchedCount--;
+                    }
+
+                    // Lengthen the array for every element, matched or not
+                    if ( seed ) {
+                        unmatched.push( elem );
+                    }
+                }
+            }
+
+            // `i` is now the count of elements visited above, and adding it to `matchedCount`
+            // makes the latter nonnegative.
+            matchedCount += i;
+
+            // Apply set filters to unmatched elements
+            // NOTE: This can be skipped if there are no unmatched elements (i.e., `matchedCount`
+            // equals `i`), unless we didn't visit _any_ elements in the above loop because we have
+            // no element matchers and no seed.
+            // Incrementing an initially-string "0" `i` allows `i` to remain a string only in that
+            // case, which will result in a "00" `matchedCount` that differs from `i` but is also
+            // numerically zero.
+            if ( bySet && i !== matchedCount ) {
+                j = 0;
+                while ( (matcher = setMatchers[j++]) ) {
+                    matcher( unmatched, setMatched, context, xml );
+                }
+
+                if ( seed ) {
+                    // Reintegrate element matches to eliminate the need for sorting
+                    if ( matchedCount > 0 ) {
+                        while ( i-- ) {
+                            if ( !(unmatched[i] || setMatched[i]) ) {
+                                setMatched[i] = pop.call( results );
+                            }
+                        }
+                    }
+
+                    // Discard index placeholder values to get only actual matches
+                    setMatched = condense( setMatched );
+                }
+
+                // Add matches to results
+                push.apply( results, setMatched );
+
+                // Seedless set matches succeeding multiple successful matchers stipulate sorting
+                if ( outermost && !seed && setMatched.length > 0 &&
+                    ( matchedCount + setMatchers.length ) > 1 ) {
+
+                    Sizzle.uniqueSort( results );
+                }
+            }
+
+            // Override manipulation of globals by nested matchers
+            if ( outermost ) {
+                dirruns = dirrunsUnique;
+                outermostContext = contextBackup;
+            }
+
+            return unmatched;
+        };
+
+    return bySet ?
+        markFunction( superMatcher ) :
+        superMatcher;
+}
+
+compile = Sizzle.compile = function( selector, match /* Internal Use Only */ ) {
+    var i,
+        setMatchers = [],
+        elementMatchers = [],
+        cached = compilerCache[ selector + " " ];
+
+    if ( !cached ) {
+        // Generate a function of recursive functions that can be used to check each element
+        if ( !match ) {
+            match = tokenize( selector );
+        }
+        i = match.length;
+        while ( i-- ) {
+            cached = matcherFromTokens( match[i] );
+            if ( cached[ expando ] ) {
+                setMatchers.push( cached );
+            } else {
+                elementMatchers.push( cached );
+            }
+        }
+
+        // Cache the compiled function
+        cached = compilerCache( selector, matcherFromGroupMatchers( elementMatchers, setMatchers ) );
+
+        // Save selector and tokenization
+        cached.selector = selector;
+    }
+    return cached;
+};
+
+/**
+ * A low-level selection function that works with Sizzle's compiled
+ *  selector functions
+ * @param {String|Function} selector A selector or a pre-compiled
+ *  selector function built with Sizzle.compile
+ * @param {Element} context
+ * @param {Array} [results]
+ * @param {Array} [seed] A set of elements to match against
+ */
+select = Sizzle.select = function( selector, context, results, seed ) {
+    var i, tokens, token, type, find,
+        compiled = typeof selector === "function" && selector,
+        match = !seed && tokenize( (selector = compiled.selector || selector) );
+
+    results = results || [];
+
+    // Try to minimize operations if there is only one selector in the list and no seed
+    // (the latter of which guarantees us context)
+    if ( match.length === 1 ) {
+
+        // Reduce context if the leading compound selector is an ID
+        tokens = match[0] = match[0].slice( 0 );
+        if ( tokens.length > 2 && (token = tokens[0]).type === "ID" &&
+                support.getById && context.nodeType === 9 && documentIsHTML &&
+                Expr.relative[ tokens[1].type ] ) {
+
+            context = ( Expr.find["ID"]( token.matches[0].replace(runescape, funescape), context ) || [] )[0];
+            if ( !context ) {
+                return results;
+
+            // Precompiled matchers will still verify ancestry, so step up a level
+            } else if ( compiled ) {
+                context = context.parentNode;
+            }
+
+            selector = selector.slice( tokens.shift().value.length );
+        }
+
+        // Fetch a seed set for right-to-left matching
+        i = matchExpr["needsContext"].test( selector ) ? 0 : tokens.length;
+        while ( i-- ) {
+            token = tokens[i];
+
+            // Abort if we hit a combinator
+            if ( Expr.relative[ (type = token.type) ] ) {
+                break;
+            }
+            if ( (find = Expr.find[ type ]) ) {
+                // Search, expanding context for leading sibling combinators
+                if ( (seed = find(
+                    token.matches[0].replace( runescape, funescape ),
+                    rsibling.test( tokens[0].type ) && testContext( context.parentNode ) || context
+                )) ) {
+
+                    // If seed is empty or no tokens remain, we can return early
+                    tokens.splice( i, 1 );
+                    selector = seed.length && toSelector( tokens );
+                    if ( !selector ) {
+                        push.apply( results, seed );
+                        return results;
+                    }
+
+                    break;
+                }
+            }
+        }
+    }
+
+    // Compile and execute a filtering function if one is not provided
+    // Provide `match` to avoid retokenization if we modified the selector above
+    ( compiled || compile( selector, match ) )(
+        seed,
+        context,
+        !documentIsHTML,
+        results,
+        !context || rsibling.test( selector ) && testContext( context.parentNode ) || context
+    );
+    return results;
+};
+
+// One-time assignments
+
+// Sort stability
+support.sortStable = expando.split("").sort( sortOrder ).join("") === expando;
+
+// Support: Chrome 14-35+
+// Always assume duplicates if they aren't passed to the comparison function
+support.detectDuplicates = !!hasDuplicate;
+
+// Initialize against the default document
+setDocument();
+
+// Support: Webkit<537.32 - Safari 6.0.3/Chrome 25 (fixed in Chrome 27)
+// Detached nodes confoundingly follow *each other*
+support.sortDetached = assert(function( div1 ) {
+    // Should return 1, but returns 4 (following)
+    return div1.compareDocumentPosition( document.createElement("div") ) & 1;
+});
+
+// Support: IE<8
+// Prevent attribute/property "interpolation"
+// http://msdn.microsoft.com/en-us/library/ms536429%28VS.85%29.aspx
+if ( !assert(function( div ) {
+    div.innerHTML = "<a href='#'></a>";
+    return div.firstChild.getAttribute("href") === "#" ;
+}) ) {
+    addHandle( "type|href|height|width", function( elem, name, isXML ) {
+        if ( !isXML ) {
+            return elem.getAttribute( name, name.toLowerCase() === "type" ? 1 : 2 );
+        }
+    });
+}
+
+// Support: IE<9
+// Use defaultValue in place of getAttribute("value")
+if ( !support.attributes || !assert(function( div ) {
+    div.innerHTML = "<input/>";
+    div.firstChild.setAttribute( "value", "" );
+    return div.firstChild.getAttribute( "value" ) === "";
+}) ) {
+    addHandle( "value", function( elem, name, isXML ) {
+        if ( !isXML && elem.nodeName.toLowerCase() === "input" ) {
+            return elem.defaultValue;
+        }
+    });
+}
+
+// Support: IE<9
+// Use getAttributeNode to fetch booleans when getAttribute lies
+if ( !assert(function( div ) {
+    return div.getAttribute("disabled") == null;
+}) ) {
+    addHandle( booleans, function( elem, name, isXML ) {
+        var val;
+        if ( !isXML ) {
+            return elem[ name ] === true ? name.toLowerCase() :
+                    (val = elem.getAttributeNode( name )) && val.specified ?
+                    val.value :
+                null;
+        }
+    });
+}
+
+return Sizzle;
+
+})( window );
+
+
+
+jQuery.find = Sizzle;
+jQuery.expr = Sizzle.selectors;
+jQuery.expr[ ":" ] = jQuery.expr.pseudos;
+jQuery.uniqueSort = jQuery.unique = Sizzle.uniqueSort;
+jQuery.text = Sizzle.getText;
+jQuery.isXMLDoc = Sizzle.isXML;
+jQuery.contains = Sizzle.contains;
+
+
+
+var dir = function( elem, dir, until ) {
+    var matched = [],
+        truncate = until !== undefined;
+
+    while ( ( elem = elem[ dir ] ) && elem.nodeType !== 9 ) {
+        if ( elem.nodeType === 1 ) {
+            if ( truncate && jQuery( elem ).is( until ) ) {
+                break;
+            }
+            matched.push( elem );
+        }
+    }
+    return matched;
+};
+
+
+var siblings = function( n, elem ) {
+    var matched = [];
+
+    for ( ; n; n = n.nextSibling ) {
+        if ( n.nodeType === 1 && n !== elem ) {
+            matched.push( n );
+        }
+    }
+
+    return matched;
+};
+
+
+var rneedsContext = jQuery.expr.match.needsContext;
+
+var rsingleTag = ( /^<([\w-]+)\s*\/?>(?:<\/\1>|)$/ );
+
+
+
+var risSimple = /^.[^:#\[\.,]*$/;
+
+// Implement the identical functionality for filter and not
+function winnow( elements, qualifier, not ) {
+    if ( jQuery.isFunction( qualifier ) ) {
+        return jQuery.grep( elements, function( elem, i ) {
+            /* jshint -W018 */
+            return !!qualifier.call( elem, i, elem ) !== not;
+        } );
+
+    }
+
+    if ( qualifier.nodeType ) {
+        return jQuery.grep( elements, function( elem ) {
+            return ( elem === qualifier ) !== not;
+        } );
+
+    }
+
+    if ( typeof qualifier === "string" ) {
+        if ( risSimple.test( qualifier ) ) {
+            return jQuery.filter( qualifier, elements, not );
+        }
+
+        qualifier = jQuery.filter( qualifier, elements );
+    }
+
+    return jQuery.grep( elements, function( elem ) {
+        return ( indexOf.call( qualifier, elem ) > -1 ) !== not;
+    } );
+}
+
+jQuery.filter = function( expr, elems, not ) {
+    var elem = elems[ 0 ];
+
+    if ( not ) {
+        expr = ":not(" + expr + ")";
+    }
+
+    return elems.length === 1 && elem.nodeType === 1 ?
+        jQuery.find.matchesSelector( elem, expr ) ? [ elem ] : [] :
+        jQuery.find.matches( expr, jQuery.grep( elems, function( elem ) {
+            return elem.nodeType === 1;
+        } ) );
+};
+
+jQuery.fn.extend( {
+    find: function( selector ) {
+        var i,
+            len = this.length,
+            ret = [],
+            self = this;
+
+        if ( typeof selector !== "string" ) {
+            return this.pushStack( jQuery( selector ).filter( function() {
+                for ( i = 0; i < len; i++ ) {
+                    if ( jQuery.contains( self[ i ], this ) ) {
+                        return true;
+                    }
+                }
+            } ) );
+        }
+
+        for ( i = 0; i < len; i++ ) {
+            jQuery.find( selector, self[ i ], ret );
+        }
+
+        // Needed because $( selector, context ) becomes $( context ).find( selector )
+        ret = this.pushStack( len > 1 ? jQuery.unique( ret ) : ret );
+        ret.selector = this.selector ? this.selector + " " + selector : selector;
+        return ret;
+    },
+    filter: function( selector ) {
+        return this.pushStack( winnow( this, selector || [], false ) );
+    },
+    not: function( selector ) {
+        return this.pushStack( winnow( this, selector || [], true ) );
+    },
+    is: function( selector ) {
+        return !!winnow(
+            this,
+
+            // If this is a positional/relative selector, check membership in the returned set
+            // so $("p:first").is("p:last") won't return true for a doc with two "p".
+            typeof selector === "string" && rneedsContext.test( selector ) ?
+                jQuery( selector ) :
+                selector || [],
+            false
+        ).length;
+    }
+} );
+
+
+// Initialize a jQuery object
+
+
+// A central reference to the root jQuery(document)
+var rootjQuery,
+
+    // A simple way to check for HTML strings
+    // Prioritize #id over <tag> to avoid XSS via location.hash (#9521)
+    // Strict HTML recognition (#11290: must start with <)
+    rquickExpr = /^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,
+
+    init = jQuery.fn.init = function( selector, context, root ) {
+        var match, elem;
+
+        // HANDLE: $(""), $(null), $(undefined), $(false)
+        if ( !selector ) {
+            return this;
+        }
+
+        // Method init() accepts an alternate rootjQuery
+        // so migrate can support jQuery.sub (gh-2101)
+        root = root || rootjQuery;
+
+        // Handle HTML strings
+        if ( typeof selector === "string" ) {
+            if ( selector[ 0 ] === "<" &&
+                selector[ selector.length - 1 ] === ">" &&
+                selector.length >= 3 ) {
+
+                // Assume that strings that start and end with <> are HTML and skip the regex check
+                match = [ null, selector, null ];
+
+            } else {
+                match = rquickExpr.exec( selector );
+            }
+
+            // Match html or make sure no context is specified for #id
+            if ( match && ( match[ 1 ] || !context ) ) {
+
+                // HANDLE: $(html) -> $(array)
+                if ( match[ 1 ] ) {
+                    context = context instanceof jQuery ? context[ 0 ] : context;
+
+                    // Option to run scripts is true for back-compat
+                    // Intentionally let the error be thrown if parseHTML is not present
+                    jQuery.merge( this, jQuery.parseHTML(
+                        match[ 1 ],
+                        context && context.nodeType ? context.ownerDocument || context : document,
+                        true
+                    ) );
+
+                    // HANDLE: $(html, props)
+                    if ( rsingleTag.test( match[ 1 ] ) && jQuery.isPlainObject( context ) ) {
+                        for ( match in context ) {
+
+                            // Properties of context are called as methods if possible
+                            if ( jQuery.isFunction( this[ match ] ) ) {
+                                this[ match ]( context[ match ] );
+
+                            // ...and otherwise set as attributes
+                            } else {
+                                this.attr( match, context[ match ] );
+                            }
+                        }
+                    }
+
+                    return this;
+
+                // HANDLE: $(#id)
+                } else {
+                    elem = document.getElementById( match[ 2 ] );
+
+                    // Support: Blackberry 4.6
+                    // gEBID returns nodes no longer in the document (#6963)
+                    if ( elem && elem.parentNode ) {
+
+                        // Inject the element directly into the jQuery object
+                        this.length = 1;
+                        this[ 0 ] = elem;
+                    }
+
+                    this.context = document;
+                    this.selector = selector;
+                    return this;
+                }
+
+            // HANDLE: $(expr, $(...))
+            } else if ( !context || context.jquery ) {
+                return ( context || root ).find( selector );
+
+            // HANDLE: $(expr, context)
+            // (which is just equivalent to: $(context).find(expr)
+            } else {
+                return this.constructor( context ).find( selector );
+            }
+
+        // HANDLE: $(DOMElement)
+        } else if ( selector.nodeType ) {
+            this.context = this[ 0 ] = selector;
+            this.length = 1;
+            return this;
+
+        // HANDLE: $(function)
+        // Shortcut for document ready
+        } else if ( jQuery.isFunction( selector ) ) {
+            return root.ready !== undefined ?
+                root.ready( selector ) :
+
+                // Execute immediately if ready is not present
+                selector( jQuery );
+        }
+
+        if ( selector.selector !== undefined ) {
+            this.selector = selector.selector;
+            this.context = selector.context;
+        }
+
+        return jQuery.makeArray( selector, this );
+    };
+
+// Give the init function the jQuery prototype for later instantiation
+init.prototype = jQuery.fn;
+
+// Initialize central reference
+rootjQuery = jQuery( document );
+
+
+var rparentsprev = /^(?:parents|prev(?:Until|All))/,
+
+    // Methods guaranteed to produce a unique set when starting from a unique set
+    guaranteedUnique = {
+        children: true,
+        contents: true,
+        next: true,
+        prev: true
+    };
+
+jQuery.fn.extend( {
+    has: function( target ) {
+        var targets = jQuery( target, this ),
+            l = targets.length;
+
+        return this.filter( function() {
+            var i = 0;
+            for ( ; i < l; i++ ) {
+                if ( jQuery.contains( this, targets[ i ] ) ) {
+                    return true;
+                }
+            }
+        } );
+    },
+
+    closest: function( selectors, context ) {
+        var cur,
+            i = 0,
+            l = this.length,
+            matched = [],
+            pos = rneedsContext.test( selectors ) || typeof selectors !== "string" ?
+                jQuery( selectors, context || this.context ) :
+                0;
+
+        for ( ; i < l; i++ ) {
+            for ( cur = this[ i ]; cur && cur !== context; cur = cur.parentNode ) {
+
+                // Always skip document fragments
+                if ( cur.nodeType < 11 && ( pos ?
+                    pos.index( cur ) > -1 :
+
+                    // Don't pass non-elements to Sizzle
+                    cur.nodeType === 1 &&
+                        jQuery.find.matchesSelector( cur, selectors ) ) ) {
+
+                    matched.push( cur );
+                    break;
+                }
+            }
+        }
+
+        return this.pushStack( matched.length > 1 ? jQuery.uniqueSort( matched ) : matched );
+    },
+
+    // Determine the position of an element within the set
+    index: function( elem ) {
+
+        // No argument, return index in parent
+        if ( !elem ) {
+            return ( this[ 0 ] && this[ 0 ].parentNode ) ? this.first().prevAll().length : -1;
+        }
+
+        // Index in selector
+        if ( typeof elem === "string" ) {
+            return indexOf.call( jQuery( elem ), this[ 0 ] );
+        }
+
+        // Locate the position of the desired element
+        return indexOf.call( this,
+
+            // If it receives a jQuery object, the first element is used
+            elem.jquery ? elem[ 0 ] : elem
+        );
+    },
+
+    add: function( selector, context ) {
+        return this.pushStack(
+            jQuery.uniqueSort(
+                jQuery.merge( this.get(), jQuery( selector, context ) )
+            )
+        );
+    },
+
+    addBack: function( selector ) {
+        return this.add( selector == null ?
+            this.prevObject : this.prevObject.filter( selector )
+        );
+    }
+} );
+
+function sibling( cur, dir ) {
+    while ( ( cur = cur[ dir ] ) && cur.nodeType !== 1 ) {}
+    return cur;
+}
+
+jQuery.each( {
+    parent: function( elem ) {
+        var parent = elem.parentNode;
+        return parent && parent.nodeType !== 11 ? parent : null;
+    },
+    parents: function( elem ) {
+        return dir( elem, "parentNode" );
+    },
+    parentsUntil: function( elem, i, until ) {
+        return dir( elem, "parentNode", until );
+    },
+    next: function( elem ) {
+        return sibling( elem, "nextSibling" );
+    },
+    prev: function( elem ) {
+        return sibling( elem, "previousSibling" );
+    },
+    nextAll: function( elem ) {
+        return dir( elem, "nextSibling" );
+    },
+    prevAll: function( elem ) {
+        return dir( elem, "previousSibling" );
+    },
+    nextUntil: function( elem, i, until ) {
+        return dir( elem, "nextSibling", until );
+    },
+    prevUntil: function( elem, i, until ) {
+        return dir( elem, "previousSibling", until );
+    },
+    siblings: function( elem ) {
+        return siblings( ( elem.parentNode || {} ).firstChild, elem );
+    },
+    children: function( elem ) {
+        return siblings( elem.firstChild );
+    },
+    contents: function( elem ) {
+        return elem.contentDocument || jQuery.merge( [], elem.childNodes );
+    }
+}, function( name, fn ) {
+    jQuery.fn[ name ] = function( until, selector ) {
+        var matched = jQuery.map( this, fn, until );
+
+        if ( name.slice( -5 ) !== "Until" ) {
+            selector = until;
+        }
+
+        if ( selector && typeof selector === "string" ) {
+            matched = jQuery.filter( selector, matched );
+        }
+
+        if ( this.length > 1 ) {
+
+            // Remove duplicates
+            if ( !guaranteedUnique[ name ] ) {
+                jQuery.uniqueSort( matched );
+            }
+
+            // Reverse order for parents* and prev-derivatives
+            if ( rparentsprev.test( name ) ) {
+                matched.reverse();
+            }
+        }
+
+        return this.pushStack( matched );
+    };
+} );
+var rnotwhite = ( /\S+/g );
+
+
+
+// Convert String-formatted options into Object-formatted ones
+function createOptions( options ) {
+    var object = {};
+    jQuery.each( options.match( rnotwhite ) || [], function( _, flag ) {
+        object[ flag ] = true;
+    } );
+    return object;
+}
+
+/*
+ * Create a callback list using the following parameters:
+ *
+ *  options: an optional list of space-separated options that will change how
+ *          the callback list behaves or a more traditional option object
+ *
+ * By default a callback list will act like an event callback list and can be
+ * "fired" multiple times.
+ *
+ * Possible options:
+ *
+ *  once:           will ensure the callback list can only be fired once (like a Deferred)
+ *
+ *  memory:         will keep track of previous values and will call any callback added
+ *                  after the list has been fired right away with the latest "memorized"
+ *                  values (like a Deferred)
+ *
+ *  unique:         will ensure a callback can only be added once (no duplicate in the list)
+ *
+ *  stopOnFalse:    interrupt callings when a callback returns false
+ *
+ */
+jQuery.Callbacks = function( options ) {
+
+    // Convert options from String-formatted to Object-formatted if needed
+    // (we check in cache first)
+    options = typeof options === "string" ?
+        createOptions( options ) :
+        jQuery.extend( {}, options );
+
+    var // Flag to know if list is currently firing
+        firing,
+
+        // Last fire value for non-forgettable lists
+        memory,
+
+        // Flag to know if list was already fired
+        fired,
+
+        // Flag to prevent firing
+        locked,
+
+        // Actual callback list
+        list = [],
+
+        // Queue of execution data for repeatable lists
+        queue = [],
+
+        // Index of currently firing callback (modified by add/remove as needed)
+        firingIndex = -1,
+
+        // Fire callbacks
+        fire = function() {
+
+            // Enforce single-firing
+            locked = options.once;
+
+            // Execute callbacks for all pending executions,
+            // respecting firingIndex overrides and runtime changes
+            fired = firing = true;
+            for ( ; queue.length; firingIndex = -1 ) {
+                memory = queue.shift();
+                while ( ++firingIndex < list.length ) {
+
+                    // Run callback and check for early termination
+                    if ( list[ firingIndex ].apply( memory[ 0 ], memory[ 1 ] ) === false &&
+                        options.stopOnFalse ) {
+
+                        // Jump to end and forget the data so .add doesn't re-fire
+                        firingIndex = list.length;
+                        memory = false;
+                    }
+                }
+            }
+
+            // Forget the data if we're done with it
+            if ( !options.memory ) {
+                memory = false;
+            }
+
+            firing = false;
+
+            // Clean up if we're done firing for good
+            if ( locked ) {
+
+                // Keep an empty list if we have data for future add calls
+                if ( memory ) {
+                    list = [];
+
+                // Otherwise, this object is spent
+                } else {
+                    list = "";
+                }
+            }
+        },
+
+        // Actual Callbacks object
+        self = {
+
+            // Add a callback or a collection of callbacks to the list
+            add: function() {
+                if ( list ) {
+
+                    // If we have memory from a past run, we should fire after adding
+                    if ( memory && !firing ) {
+                        firingIndex = list.length - 1;
+                        queue.push( memory );
+                    }
+
+                    ( function add( args ) {
+                        jQuery.each( args, function( _, arg ) {
+                            if ( jQuery.isFunction( arg ) ) {
+                                if ( !options.unique || !self.has( arg ) ) {
+                                    list.push( arg );
+                                }
+                            } else if ( arg && arg.length && jQuery.type( arg ) !== "string" ) {
+
+                                // Inspect recursively
+                                add( arg );
+                            }
+                        } );
+                    } )( arguments );
+
+                    if ( memory && !firing ) {
+                        fire();
+                    }
+                }
+                return this;
+            },
+
+            // Remove a callback from the list
+            remove: function() {
+                jQuery.each( arguments, function( _, arg ) {
+                    var index;
+                    while ( ( index = jQuery.inArray( arg, list, index ) ) > -1 ) {
+                        list.splice( index, 1 );
+
+                        // Handle firing indexes
+                        if ( index <= firingIndex ) {
+                            firingIndex--;
+                        }
+                    }
+                } );
+                return this;
+            },
+
+            // Check if a given callback is in the list.
+            // If no argument is given, return whether or not list has callbacks attached.
+            has: function( fn ) {
+                return fn ?
+                    jQuery.inArray( fn, list ) > -1 :
+                    list.length > 0;
+            },
+
+            // Remove all callbacks from the list
+            empty: function() {
+                if ( list ) {
+                    list = [];
+                }
+                return this;
+            },
+
+            // Disable .fire and .add
+            // Abort any current/pending executions
+            // Clear all callbacks and values
+            disable: function() {
+                locked = queue = [];
+                list = memory = "";
+                return this;
+            },
+            disabled: function() {
+                return !list;
+            },
+
+            // Disable .fire
+            // Also disable .add unless we have memory (since it would have no effect)
+            // Abort any pending executions
+            lock: function() {
+                locked = queue = [];
+                if ( !memory ) {
+                    list = memory = "";
+                }
+                return this;
+            },
+            locked: function() {
+                return !!locked;
+            },
+
+            // Call all callbacks with the given context and arguments
+            fireWith: function( context, args ) {
+                if ( !locked ) {
+                    args = args || [];
+                    args = [ context, args.slice ? args.slice() : args ];
+                    queue.push( args );
+                    if ( !firing ) {
+                        fire();
+                    }
+                }
+                return this;
+            },
+
+            // Call all the callbacks with the given arguments
+            fire: function() {
+                self.fireWith( this, arguments );
+                return this;
+            },
+
+            // To know if the callbacks have already been called at least once
+            fired: function() {
+                return !!fired;
+            }
+        };
+
+    return self;
+};
+
+
+jQuery.extend( {
+
+    Deferred: function( func ) {
+        var tuples = [
+
+                // action, add listener, listener list, final state
+                [ "resolve", "done", jQuery.Callbacks( "once memory" ), "resolved" ],
+                [ "reject", "fail", jQuery.Callbacks( "once memory" ), "rejected" ],
+                [ "notify", "progress", jQuery.Callbacks( "memory" ) ]
+            ],
+            state = "pending",
+            promise = {
+                state: function() {
+                    return state;
+                },
+                always: function() {
+                    deferred.done( arguments ).fail( arguments );
+                    return this;
+                },
+                then: function( /* fnDone, fnFail, fnProgress */ ) {
+                    var fns = arguments;
+                    return jQuery.Deferred( function( newDefer ) {
+                        jQuery.each( tuples, function( i, tuple ) {
+                            var fn = jQuery.isFunction( fns[ i ] ) && fns[ i ];
+
+                            // deferred[ done | fail | progress ] for forwarding actions to newDefer
+                            deferred[ tuple[ 1 ] ]( function() {
+                                var returned = fn && fn.apply( this, arguments );
+                                if ( returned && jQuery.isFunction( returned.promise ) ) {
+                                    returned.promise()
+                                        .progress( newDefer.notify )
+                                        .done( newDefer.resolve )
+                                        .fail( newDefer.reject );
+                                } else {
+                                    newDefer[ tuple[ 0 ] + "With" ](
+                                        this === promise ? newDefer.promise() : this,
+                                        fn ? [ returned ] : arguments
+                                    );
+                                }
+                            } );
+                        } );
+                        fns = null;
+                    } ).promise();
+                },
+
+                // Get a promise for this deferred
+                // If obj is provided, the promise aspect is added to the object
+                promise: function( obj ) {
+                    return obj != null ? jQuery.extend( obj, promise ) : promise;
+                }
+            },
+            deferred = {};
+
+        // Keep pipe for back-compat
+        promise.pipe = promise.then;
+
+        // Add list-specific methods
+        jQuery.each( tuples, function( i, tuple ) {
+            var list = tuple[ 2 ],
+                stateString = tuple[ 3 ];
+
+            // promise[ done | fail | progress ] = list.add
+            promise[ tuple[ 1 ] ] = list.add;
+
+            // Handle state
+            if ( stateString ) {
+                list.add( function() {
+
+                    // state = [ resolved | rejected ]
+                    state = stateString;
+
+                // [ reject_list | resolve_list ].disable; progress_list.lock
+                }, tuples[ i ^ 1 ][ 2 ].disable, tuples[ 2 ][ 2 ].lock );
+            }
+
+            // deferred[ resolve | reject | notify ]
+            deferred[ tuple[ 0 ] ] = function() {
+                deferred[ tuple[ 0 ] + "With" ]( this === deferred ? promise : this, arguments );
+                return this;
+            };
+            deferred[ tuple[ 0 ] + "With" ] = list.fireWith;
+        } );
+
+        // Make the deferred a promise
+        promise.promise( deferred );
+
+        // Call given func if any
+        if ( func ) {
+            func.call( deferred, deferred );
+        }
+
+        // All done!
+        return deferred;
+    },
+
+    // Deferred helper
+    when: function( subordinate /* , ..., subordinateN */ ) {
+        var i = 0,
+            resolveValues = slice.call( arguments ),
+            length = resolveValues.length,
+
+            // the count of uncompleted subordinates
+            remaining = length !== 1 ||
+                ( subordinate && jQuery.isFunction( subordinate.promise ) ) ? length : 0,
+
+            // the master Deferred.
+            // If resolveValues consist of only a single Deferred, just use that.
+            deferred = remaining === 1 ? subordinate : jQuery.Deferred(),
+
+            // Update function for both resolve and progress values
+            updateFunc = function( i, contexts, values ) {
+                return function( value ) {
+                    contexts[ i ] = this;
+                    values[ i ] = arguments.length > 1 ? slice.call( arguments ) : value;
+                    if ( values === progressValues ) {
+                        deferred.notifyWith( contexts, values );
+                    } else if ( !( --remaining ) ) {
+                        deferred.resolveWith( contexts, values );
+                    }
+                };
+            },
+
+            progressValues, progressContexts, resolveContexts;
+
+        // Add listeners to Deferred subordinates; treat others as resolved
+        if ( length > 1 ) {
+            progressValues = new Array( length );
+            progressContexts = new Array( length );
+            resolveContexts = new Array( length );
+            for ( ; i < length; i++ ) {
+                if ( resolveValues[ i ] && jQuery.isFunction( resolveValues[ i ].promise ) ) {
+                    resolveValues[ i ].promise()
+                        .progress( updateFunc( i, progressContexts, progressValues ) )
+                        .done( updateFunc( i, resolveContexts, resolveValues ) )
+                        .fail( deferred.reject );
+                } else {
+                    --remaining;
+                }
+            }
+        }
+
+        // If we're not waiting on anything, resolve the master
+        if ( !remaining ) {
+            deferred.resolveWith( resolveContexts, resolveValues );
+        }
+
+        return deferred.promise();
+    }
+} );
+
+
+// The deferred used on DOM ready
+var readyList;
+
+jQuery.fn.ready = function( fn ) {
+
+    // Add the callback
+    jQuery.ready.promise().done( fn );
+
+    return this;
+};
+
+jQuery.extend( {
+
+    // Is the DOM ready to be used? Set to true once it occurs.
+    isReady: false,
+
+    // A counter to track how many items to wait for before
+    // the ready event fires. See #6781
+    readyWait: 1,
+
+    // Hold (or release) the ready event
+    holdReady: function( hold ) {
+        if ( hold ) {
+            jQuery.readyWait++;
+        } else {
+            jQuery.ready( true );
+        }
+    },
+
+    // Handle when the DOM is ready
+    ready: function( wait ) {
+
+        // Abort if there are pending holds or we're already ready
+        if ( wait === true ? --jQuery.readyWait : jQuery.isReady ) {
+            return;
+        }
+
+        // Remember that the DOM is ready
+        jQuery.isReady = true;
+
+        // If a normal DOM Ready event fired, decrement, and wait if need be
+        if ( wait !== true && --jQuery.readyWait > 0 ) {
+            return;
+        }
+
+        // If there are functions bound, to execute
+        readyList.resolveWith( document, [ jQuery ] );
+
+        // Trigger any bound ready events
+        if ( jQuery.fn.triggerHandler ) {
+            jQuery( document ).triggerHandler( "ready" );
+            jQuery( document ).off( "ready" );
+        }
+    }
+} );
+
+/**
+ * The ready event handler and self cleanup method
+ */
+function completed() {
+    document.removeEventListener( "DOMContentLoaded", completed );
+    window.removeEventListener( "load", completed );
+    jQuery.ready();
+}
+
+jQuery.ready.promise = function( obj ) {
+    if ( !readyList ) {
+
+        readyList = jQuery.Deferred();
+
+        // Catch cases where $(document).ready() is called
+        // after the browser event has already occurred.
+        // Support: IE9-10 only
+        // Older IE sometimes signals "interactive" too soon
+        if ( document.readyState === "complete" ||
+            ( document.readyState !== "loading" && !document.documentElement.doScroll ) ) {
+
+            // Handle it asynchronously to allow scripts the opportunity to delay ready
+            window.setTimeout( jQuery.ready );
+
+        } else {
+
+            // Use the handy event callback
+            document.addEventListener( "DOMContentLoaded", completed );
+
+            // A fallback to window.onload, that will always work
+            window.addEventListener( "load", completed );
+        }
+    }
+    return readyList.promise( obj );
+};
+
+// Kick off the DOM ready check even if the user does not
+jQuery.ready.promise();
+
+
+
+
+// Multifunctional method to get and set values of a collection
+// The value/s can optionally be executed if it's a function
+var access = function( elems, fn, key, value, chainable, emptyGet, raw ) {
+    var i = 0,
+        len = elems.length,
+        bulk = key == null;
+
+    // Sets many values
+    if ( jQuery.type( key ) === "object" ) {
+        chainable = true;
+        for ( i in key ) {
+            access( elems, fn, i, key[ i ], true, emptyGet, raw );
+        }
+
+    // Sets one value
+    } else if ( value !== undefined ) {
+        chainable = true;
+
+        if ( !jQuery.isFunction( value ) ) {
+            raw = true;
+        }
+
+        if ( bulk ) {
+
+            // Bulk operations run against the entire set
+            if ( raw ) {
+                fn.call( elems, value );
+                fn = null;
+
+            // ...except when executing function values
+            } else {
+                bulk = fn;
+                fn = function( elem, key, value ) {
+                    return bulk.call( jQuery( elem ), value );
+                };
+            }
+        }
+
+        if ( fn ) {
+            for ( ; i < len; i++ ) {
+                fn(
+                    elems[ i ], key, raw ?
+                    value :
+                    value.call( elems[ i ], i, fn( elems[ i ], key ) )
+                );
+            }
+        }
+    }
+
+    return chainable ?
+        elems :
+
+        // Gets
+        bulk ?
+            fn.call( elems ) :
+            len ? fn( elems[ 0 ], key ) : emptyGet;
+};
+var acceptData = function( owner ) {
+
+    // Accepts only:
+    //  - Node
+    //    - Node.ELEMENT_NODE
+    //    - Node.DOCUMENT_NODE
+    //  - Object
+    //    - Any
+    /* jshint -W018 */
+    return owner.nodeType === 1 || owner.nodeType === 9 || !( +owner.nodeType );
+};
+
+
+
+
+function Data() {
+    this.expando = jQuery.expando + Data.uid++;
+}
+
+Data.uid = 1;
+
+Data.prototype = {
+
+    register: function( owner, initial ) {
+        var value = initial || {};
+
+        // If it is a node unlikely to be stringify-ed or looped over
+        // use plain assignment
+        if ( owner.nodeType ) {
+            owner[ this.expando ] = value;
+
+        // Otherwise secure it in a non-enumerable, non-writable property
+        // configurability must be true to allow the property to be
+        // deleted with the delete operator
+        } else {
+            Object.defineProperty( owner, this.expando, {
+                value: value,
+                writable: true,
+                configurable: true
+            } );
+        }
+        return owner[ this.expando ];
+    },
+    cache: function( owner ) {
+
+        // We can accept data for non-element nodes in modern browsers,
+        // but we should not, see #8335.
+        // Always return an empty object.
+        if ( !acceptData( owner ) ) {
+            return {};
+        }
+
+        // Check if the owner object already has a cache
+        var value = owner[ this.expando ];
+
+        // If not, create one
+        if ( !value ) {
+            value = {};
+
+            // We can accept data for non-element nodes in modern browsers,
+            // but we should not, see #8335.
+            // Always return an empty object.
+            if ( acceptData( owner ) ) {
+
+                // If it is a node unlikely to be stringify-ed or looped over
+                // use plain assignment
+                if ( owner.nodeType ) {
+                    owner[ this.expando ] = value;
+
+                // Otherwise secure it in a non-enumerable property
+                // configurable must be true to allow the property to be
+                // deleted when data is removed
+                } else {
+                    Object.defineProperty( owner, this.expando, {
+                        value: value,
+                        configurable: true
+                    } );
+                }
+            }
+        }
+
+        return value;
+    },
+    set: function( owner, data, value ) {
+        var prop,
+            cache = this.cache( owner );
+
+        // Handle: [ owner, key, value ] args
+        if ( typeof data === "string" ) {
+            cache[ data ] = value;
+
+        // Handle: [ owner, { properties } ] args
+        } else {
+
+            // Copy the properties one-by-one to the cache object
+            for ( prop in data ) {
+                cache[ prop ] = data[ prop ];
+            }
+        }
+        return cache;
+    },
+    get: function( owner, key ) {
+        return key === undefined ?
+            this.cache( owner ) :
+            owner[ this.expando ] && owner[ this.expando ][ key ];
+    },
+    access: function( owner, key, value ) {
+        var stored;
+
+        // In cases where either:
+        //
+        //   1. No key was specified
+        //   2. A string key was specified, but no value provided
+        //
+        // Take the "read" path and allow the get method to determine
+        // which value to return, respectively either:
+        //
+        //   1. The entire cache object
+        //   2. The data stored at the key
+        //
+        if ( key === undefined ||
+                ( ( key && typeof key === "string" ) && value === undefined ) ) {
+
+            stored = this.get( owner, key );
+
+            return stored !== undefined ?
+                stored : this.get( owner, jQuery.camelCase( key ) );
+        }
+
+        // When the key is not a string, or both a key and value
+        // are specified, set or extend (existing objects) with either:
+        //
+        //   1. An object of properties
+        //   2. A key and value
+        //
+        this.set( owner, key, value );
+
+        // Since the "set" path can have two possible entry points
+        // return the expected data based on which path was taken[*]
+        return value !== undefined ? value : key;
+    },
+    remove: function( owner, key ) {
+        var i, name, camel,
+            cache = owner[ this.expando ];
+
+        if ( cache === undefined ) {
+            return;
+        }
+
+        if ( key === undefined ) {
+            this.register( owner );
+
+        } else {
+
+            // Support array or space separated string of keys
+            if ( jQuery.isArray( key ) ) {
+
+                // If "name" is an array of keys...
+                // When data is initially created, via ("key", "val") signature,
+                // keys will be converted to camelCase.
+                // Since there is no way to tell _how_ a key was added, remove
+                // both plain key and camelCase key. #12786
+                // This will only penalize the array argument path.
+                name = key.concat( key.map( jQuery.camelCase ) );
+            } else {
+                camel = jQuery.camelCase( key );
+
+                // Try the string as a key before any manipulation
+                if ( key in cache ) {
+                    name = [ key, camel ];
+                } else {
+
+                    // If a key with the spaces exists, use it.
+                    // Otherwise, create an array by matching non-whitespace
+                    name = camel;
+                    name = name in cache ?
+                        [ name ] : ( name.match( rnotwhite ) || [] );
+                }
+            }
+
+            i = name.length;
+
+            while ( i-- ) {
+                delete cache[ name[ i ] ];
+            }
+        }
+
+        // Remove the expando if there's no more data
+        if ( key === undefined || jQuery.isEmptyObject( cache ) ) {
+
+            // Support: Chrome <= 35-45+
+            // Webkit & Blink performance suffers when deleting properties
+            // from DOM nodes, so set to undefined instead
+            // https://code.google.com/p/chromium/issues/detail?id=378607
+            if ( owner.nodeType ) {
+                owner[ this.expando ] = undefined;
+            } else {
+                delete owner[ this.expando ];
+            }
+        }
+    },
+    hasData: function( owner ) {
+        var cache = owner[ this.expando ];
+        return cache !== undefined && !jQuery.isEmptyObject( cache );
+    }
+};
+var dataPriv = new Data();
+
+var dataUser = new Data();
+
+
+
+//  Implementation Summary
+//
+//  1. Enforce API surface and semantic compatibility with 1.9.x branch
+//  2. Improve the module's maintainability by reducing the storage
+//      paths to a single mechanism.
+//  3. Use the same single mechanism to support "private" and "user" data.
+//  4. _Never_ expose "private" data to user code (TODO: Drop _data, _removeData)
+//  5. Avoid exposing implementation details on user objects (eg. expando properties)
+//  6. Provide a clear path for implementation upgrade to WeakMap in 2014
+
+var rbrace = /^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,
+    rmultiDash = /[A-Z]/g;
+
+function dataAttr( elem, key, data ) {
+    var name;
+
+    // If nothing was found internally, try to fetch any
+    // data from the HTML5 data-* attribute
+    if ( data === undefined && elem.nodeType === 1 ) {
+        name = "data-" + key.replace( rmultiDash, "-$&" ).toLowerCase();
+        data = elem.getAttribute( name );
+
+        if ( typeof data === "string" ) {
+            try {
+                data = data === "true" ? true :
+                    data === "false" ? false :
+                    data === "null" ? null :
+
+                    // Only convert to a number if it doesn't change the string
+                    +data + "" === data ? +data :
+                    rbrace.test( data ) ? jQuery.parseJSON( data ) :
+                    data;
+            } catch ( e ) {}
+
+            // Make sure we set the data so it isn't changed later
+            dataUser.set( elem, key, data );
+        } else {
+            data = undefined;
+        }
+    }
+    return data;
+}
+
+jQuery.extend( {
+    hasData: function( elem ) {
+        return dataUser.hasData( elem ) || dataPriv.hasData( elem );
+    },
+
+    data: function( elem, name, data ) {
+        return dataUser.access( elem, name, data );
+    },
+
+    removeData: function( elem, name ) {
+        dataUser.remove( elem, name );
+    },
+
+    // TODO: Now that all calls to _data and _removeData have been replaced
+    // with direct calls to dataPriv methods, these can be deprecated.
+    _data: function( elem, name, data ) {
+        return dataPriv.access( elem, name, data );
+    },
+
+    _removeData: function( elem, name ) {
+        dataPriv.remove( elem, name );
+    }
+} );
+
+jQuery.fn.extend( {
+    data: function( key, value ) {
+        var i, name, data,
+            elem = this[ 0 ],
+            attrs = elem && elem.attributes;
+
+        // Gets all values
+        if ( key === undefined ) {
+            if ( this.length ) {
+                data = dataUser.get( elem );
+
+                if ( elem.nodeType === 1 && !dataPriv.get( elem, "hasDataAttrs" ) ) {
+                    i = attrs.length;
+                    while ( i-- ) {
+
+                        // Support: IE11+
+                        // The attrs elements can be null (#14894)
+                        if ( attrs[ i ] ) {
+                            name = attrs[ i ].name;
+                            if ( name.indexOf( "data-" ) === 0 ) {
+                                name = jQuery.camelCase( name.slice( 5 ) );
+                                dataAttr( elem, name, data[ name ] );
+                            }
+                        }
+                    }
+                    dataPriv.set( elem, "hasDataAttrs", true );
+                }
+            }
+
+            return data;
+        }
+
+        // Sets multiple values
+        if ( typeof key === "object" ) {
+            return this.each( function() {
+                dataUser.set( this, key );
+            } );
+        }
+
+        return access( this, function( value ) {
+            var data, camelKey;
+
+            // The calling jQuery object (element matches) is not empty
+            // (and therefore has an element appears at this[ 0 ]) and the
+            // `value` parameter was not undefined. An empty jQuery object
+            // will result in `undefined` for elem = this[ 0 ] which will
+            // throw an exception if an attempt to read a data cache is made.
+            if ( elem && value === undefined ) {
+
+                // Attempt to get data from the cache
+                // with the key as-is
+                data = dataUser.get( elem, key ) ||
+
+                    // Try to find dashed key if it exists (gh-2779)
+                    // This is for 2.2.x only
+                    dataUser.get( elem, key.replace( rmultiDash, "-$&" ).toLowerCase() );
+
+                if ( data !== undefined ) {
+                    return data;
+                }
+
+                camelKey = jQuery.camelCase( key );
+
+                // Attempt to get data from the cache
+                // with the key camelized
+                data = dataUser.get( elem, camelKey );
+                if ( data !== undefined ) {
+                    return data;
+                }
+
+                // Attempt to "discover" the data in
+                // HTML5 custom data-* attrs
+                data = dataAttr( elem, camelKey, undefined );
+                if ( data !== undefined ) {
+                    return data;
+                }
+
+                // We tried really hard, but the data doesn't exist.
+                return;
+            }
+
+            // Set the data...
+            camelKey = jQuery.camelCase( key );
+            this.each( function() {
+
+                // First, attempt to store a copy or reference of any
+                // data that might've been store with a camelCased key.
+                var data = dataUser.get( this, camelKey );
+
+                // For HTML5 data-* attribute interop, we have to
+                // store property names with dashes in a camelCase form.
+                // This might not apply to all properties...*
+                dataUser.set( this, camelKey, value );
+
+                // *... In the case of properties that might _actually_
+                // have dashes, we need to also store a copy of that
+                // unchanged property.
+                if ( key.indexOf( "-" ) > -1 && data !== undefined ) {
+                    dataUser.set( this, key, value );
+                }
+            } );
+        }, null, value, arguments.length > 1, null, true );
+    },
+
+    removeData: function( key ) {
+        return this.each( function() {
+            dataUser.remove( this, key );
+        } );
+    }
+} );
+
+
+jQuery.extend( {
+    queue: function( elem, type, data ) {
+        var queue;
+
+        if ( elem ) {
+            type = ( type || "fx" ) + "queue";
+            queue = dataPriv.get( elem, type );
+
+            // Speed up dequeue by getting out quickly if this is just a lookup
+            if ( data ) {
+                if ( !queue || jQuery.isArray( data ) ) {
+                    queue = dataPriv.access( elem, type, jQuery.makeArray( data ) );
+                } else {
+                    queue.push( data );
+                }
+            }
+            return queue || [];
+        }
+    },
+
+    dequeue: function( elem, type ) {
+        type = type || "fx";
+
+        var queue = jQuery.queue( elem, type ),
+            startLength = queue.length,
+            fn = queue.shift(),
+            hooks = jQuery._queueHooks( elem, type ),
+            next = function() {
+                jQuery.dequeue( elem, type );
+            };
+
+        // If the fx queue is dequeued, always remove the progress sentinel
+        if ( fn === "inprogress" ) {
+            fn = queue.shift();
+            startLength--;
+        }
+
+        if ( fn ) {
+
+            // Add a progress sentinel to prevent the fx queue from being
+            // automatically dequeued
+            if ( type === "fx" ) {
+                queue.unshift( "inprogress" );
+            }
+
+            // Clear up the last queue stop function
+            delete hooks.stop;
+            fn.call( elem, next, hooks );
+        }
+
+        if ( !startLength && hooks ) {
+            hooks.empty.fire();
+        }
+    },
+
+    // Not public - generate a queueHooks object, or return the current one
+    _queueHooks: function( elem, type ) {
+        var key = type + "queueHooks";
+        return dataPriv.get( elem, key ) || dataPriv.access( elem, key, {
+            empty: jQuery.Callbacks( "once memory" ).add( function() {
+                dataPriv.remove( elem, [ type + "queue", key ] );
+            } )
+        } );
+    }
+} );
+
+jQuery.fn.extend( {
+    queue: function( type, data ) {
+        var setter = 2;
+
+        if ( typeof type !== "string" ) {
+            data = type;
+            type = "fx";
+            setter--;
+        }
+
+        if ( arguments.length < setter ) {
+            return jQuery.queue( this[ 0 ], type );
+        }
+
+        return data === undefined ?
+            this :
+            this.each( function() {
+                var queue = jQuery.queue( this, type, data );
+
+                // Ensure a hooks for this queue
+                jQuery._queueHooks( this, type );
+
+                if ( type === "fx" && queue[ 0 ] !== "inprogress" ) {
+                    jQuery.dequeue( this, type );
+                }
+            } );
+    },
+    dequeue: function( type ) {
+        return this.each( function() {
+            jQuery.dequeue( this, type );
+        } );
+    },
+    clearQueue: function( type ) {
+        return this.queue( type || "fx", [] );
+    },
+
+    // Get a promise resolved when queues of a certain type
+    // are emptied (fx is the type by default)
+    promise: function( type, obj ) {
+        var tmp,
+            count = 1,
+            defer = jQuery.Deferred(),
+            elements = this,
+            i = this.length,
+            resolve = function() {
+                if ( !( --count ) ) {
+                    defer.resolveWith( elements, [ elements ] );
+                }
+            };
+
+        if ( typeof type !== "string" ) {
+            obj = type;
+            type = undefined;
+        }
+        type = type || "fx";
+
+        while ( i-- ) {
+            tmp = dataPriv.get( elements[ i ], type + "queueHooks" );
+            if ( tmp && tmp.empty ) {
+                count++;
+                tmp.empty.add( resolve );
+            }
+        }
+        resolve();
+        return defer.promise( obj );
+    }
+} );
+var pnum = ( /[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/ ).source;
+
+var rcssNum = new RegExp( "^(?:([+-])=|)(" + pnum + ")([a-z%]*)$", "i" );
+
+
+var cssExpand = [ "Top", "Right", "Bottom", "Left" ];
+
+var isHidden = function( elem, el ) {
+
+        // isHidden might be called from jQuery#filter function;
+        // in that case, element will be second argument
+        elem = el || elem;
+        return jQuery.css( elem, "display" ) === "none" ||
+            !jQuery.contains( elem.ownerDocument, elem );
+    };
+
+
+
+function adjustCSS( elem, prop, valueParts, tween ) {
+    var adjusted,
+        scale = 1,
+        maxIterations = 20,
+        currentValue = tween ?
+            function() { return tween.cur(); } :
+            function() { return jQuery.css( elem, prop, "" ); },
+        initial = currentValue(),
+        unit = valueParts && valueParts[ 3 ] || ( jQuery.cssNumber[ prop ] ? "" : "px" ),
+
+        // Starting value computation is required for potential unit mismatches
+        initialInUnit = ( jQuery.cssNumber[ prop ] || unit !== "px" && +initial ) &&
+            rcssNum.exec( jQuery.css( elem, prop ) );
+
+    if ( initialInUnit && initialInUnit[ 3 ] !== unit ) {
+
+        // Trust units reported by jQuery.css
+        unit = unit || initialInUnit[ 3 ];
+
+        // Make sure we update the tween properties later on
+        valueParts = valueParts || [];
+
+        // Iteratively approximate from a nonzero starting point
+        initialInUnit = +initial || 1;
+
+        do {
+
+            // If previous iteration zeroed out, double until we get *something*.
+            // Use string for doubling so we don't accidentally see scale as unchanged below
+            scale = scale || ".5";
+
+            // Adjust and apply
+            initialInUnit = initialInUnit / scale;
+            jQuery.style( elem, prop, initialInUnit + unit );
+
+        // Update scale, tolerating zero or NaN from tween.cur()
+        // Break the loop if scale is unchanged or perfect, or if we've just had enough.
+        } while (
+            scale !== ( scale = currentValue() / initial ) && scale !== 1 && --maxIterations
+        );
+    }
+
+    if ( valueParts ) {
+        initialInUnit = +initialInUnit || +initial || 0;
+
+        // Apply relative offset (+=/-=) if specified
+        adjusted = valueParts[ 1 ] ?
+            initialInUnit + ( valueParts[ 1 ] + 1 ) * valueParts[ 2 ] :
+            +valueParts[ 2 ];
+        if ( tween ) {
+            tween.unit = unit;
+            tween.start = initialInUnit;
+            tween.end = adjusted;
+        }
+    }
+    return adjusted;
+}
+var rcheckableType = ( /^(?:checkbox|radio)$/i );
+
+var rtagName = ( /<([\w:-]+)/ );
+
+var rscriptType = ( /^$|\/(?:java|ecma)script/i );
+
+
+
+// We have to close these tags to support XHTML (#13200)
+var wrapMap = {
+
+    // Support: IE9
+    option: [ 1, "<select multiple='multiple'>", "</select>" ],
+
+    // XHTML parsers do not magically insert elements in the
+    // same way that tag soup parsers do. So we cannot shorten
+    // this by omitting <tbody> or other required elements.
+    thead: [ 1, "<table>", "</table>" ],
+    col: [ 2, "<table><colgroup>", "</colgroup></table>" ],
+    tr: [ 2, "<table><tbody>", "</tbody></table>" ],
+    td: [ 3, "<table><tbody><tr>", "</tr></tbody></table>" ],
+
+    _default: [ 0, "", "" ]
+};
+
+// Support: IE9
+wrapMap.optgroup = wrapMap.option;
+
+wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead;
+wrapMap.th = wrapMap.td;
+
+
+function getAll( context, tag ) {
+
+    // Support: IE9-11+
+    // Use typeof to avoid zero-argument method invocation on host objects (#15151)
+    var ret = typeof context.getElementsByTagName !== "undefined" ?
+            context.getElementsByTagName( tag || "*" ) :
+            typeof context.querySelectorAll !== "undefined" ?
+                context.querySelectorAll( tag || "*" ) :
+            [];
+
+    return tag === undefined || tag && jQuery.nodeName( context, tag ) ?
+        jQuery.merge( [ context ], ret ) :
+        ret;
+}
+
+
+// Mark scripts as having already been evaluated
+function setGlobalEval( elems, refElements ) {
+    var i = 0,
+        l = elems.length;
+
+    for ( ; i < l; i++ ) {
+        dataPriv.set(
+            elems[ i ],
+            "globalEval",
+            !refElements || dataPriv.get( refElements[ i ], "globalEval" )
+        );
+    }
+}
+
+
+var rhtml = /<|&#?\w+;/;
+
+function buildFragment( elems, context, scripts, selection, ignored ) {
+    var elem, tmp, tag, wrap, contains, j,
+        fragment = context.createDocumentFragment(),
+        nodes = [],
+        i = 0,
+        l = elems.length;
+
+    for ( ; i < l; i++ ) {
+        elem = elems[ i ];
+
+        if ( elem || elem === 0 ) {
+
+            // Add nodes directly
+            if ( jQuery.type( elem ) === "object" ) {
+
+                // Support: Android<4.1, PhantomJS<2
+                // push.apply(_, arraylike) throws on ancient WebKit
+                jQuery.merge( nodes, elem.nodeType ? [ elem ] : elem );
+
+            // Convert non-html into a text node
+            } else if ( !rhtml.test( elem ) ) {
+                nodes.push( context.createTextNode( elem ) );
+
+            // Convert html into DOM nodes
+            } else {
+                tmp = tmp || fragment.appendChild( context.createElement( "div" ) );
+
+                // Deserialize a standard representation
+                tag = ( rtagName.exec( elem ) || [ "", "" ] )[ 1 ].toLowerCase();
+                wrap = wrapMap[ tag ] || wrapMap._default;
+                tmp.innerHTML = wrap[ 1 ] + jQuery.htmlPrefilter( elem ) + wrap[ 2 ];
+
+                // Descend through wrappers to the right content
+                j = wrap[ 0 ];
+                while ( j-- ) {
+                    tmp = tmp.lastChild;
+                }
+
+                // Support: Android<4.1, PhantomJS<2
+                // push.apply(_, arraylike) throws on ancient WebKit
+                jQuery.merge( nodes, tmp.childNodes );
+
+                // Remember the top-level container
+                tmp = fragment.firstChild;
+
+                // Ensure the created nodes are orphaned (#12392)
+                tmp.textContent = "";
+            }
+        }
+    }
+
+    // Remove wrapper from fragment
+    fragment.textContent = "";
+
+    i = 0;
+    while ( ( elem = nodes[ i++ ] ) ) {
+
+        // Skip elements already in the context collection (trac-4087)
+        if ( selection && jQuery.inArray( elem, selection ) > -1 ) {
+            if ( ignored ) {
+                ignored.push( elem );
+            }
+            continue;
+        }
+
+        contains = jQuery.contains( elem.ownerDocument, elem );
+
+        // Append to fragment
+        tmp = getAll( fragment.appendChild( elem ), "script" );
+
+        // Preserve script evaluation history
+        if ( contains ) {
+            setGlobalEval( tmp );
+        }
+
+        // Capture executables
+        if ( scripts ) {
+            j = 0;
+            while ( ( elem = tmp[ j++ ] ) ) {
+                if ( rscriptType.test( elem.type || "" ) ) {
+                    scripts.push( elem );
+                }
+            }
+        }
+    }
+
+    return fragment;
+}
+
+
+( function() {
+    var fragment = document.createDocumentFragment(),
+        div = fragment.appendChild( document.createElement( "div" ) ),
+        input = document.createElement( "input" );
+
+    // Support: Android 4.0-4.3, Safari<=5.1
+    // Check state lost if the name is set (#11217)
+    // Support: Windows Web Apps (WWA)
+    // `name` and `type` must use .setAttribute for WWA (#14901)
+    input.setAttribute( "type", "radio" );
+    input.setAttribute( "checked", "checked" );
+    input.setAttribute( "name", "t" );
+
+    div.appendChild( input );
+
+    // Support: Safari<=5.1, Android<4.2
+    // Older WebKit doesn't clone checked state correctly in fragments
+    support.checkClone = div.cloneNode( true ).cloneNode( true ).lastChild.checked;
+
+    // Support: IE<=11+
+    // Make sure textarea (and checkbox) defaultValue is properly cloned
+    div.innerHTML = "<textarea>x</textarea>";
+    support.noCloneChecked = !!div.cloneNode( true ).lastChild.defaultValue;
+} )();
+
+
+var
+    rkeyEvent = /^key/,
+    rmouseEvent = /^(?:mouse|pointer|contextmenu|drag|drop)|click/,
+    rtypenamespace = /^([^.]*)(?:\.(.+)|)/;
+
+function returnTrue() {
+    return true;
+}
+
+function returnFalse() {
+    return false;
+}
+
+// Support: IE9
+// See #13393 for more info
+function safeActiveElement() {
+    try {
+        return document.activeElement;
+    } catch ( err ) { }
+}
+
+function on( elem, types, selector, data, fn, one ) {
+    var origFn, type;
+
+    // Types can be a map of types/handlers
+    if ( typeof types === "object" ) {
+
+        // ( types-Object, selector, data )
+        if ( typeof selector !== "string" ) {
+
+            // ( types-Object, data )
+            data = data || selector;
+            selector = undefined;
+        }
+        for ( type in types ) {
+            on( elem, type, selector, data, types[ type ], one );
+        }
+        return elem;
+    }
+
+    if ( data == null && fn == null ) {
+
+        // ( types, fn )
+        fn = selector;
+        data = selector = undefined;
+    } else if ( fn == null ) {
+        if ( typeof selector === "string" ) {
+
+            // ( types, selector, fn )
+            fn = data;
+            data = undefined;
+        } else {
+
+            // ( types, data, fn )
+            fn = data;
+            data = selector;
+            selector = undefined;
+        }
+    }
+    if ( fn === false ) {
+        fn = returnFalse;
+    } else if ( !fn ) {
+        return elem;
+    }
+
+    if ( one === 1 ) {
+        origFn = fn;
+        fn = function( event ) {
+
+            // Can use an empty set, since event contains the info
+            jQuery().off( event );
+            return origFn.apply( this, arguments );
+        };
+
+        // Use same guid so caller can remove using origFn
+        fn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ );
+    }
+    return elem.each( function() {
+        jQuery.event.add( this, types, fn, data, selector );
+    } );
+}
+
+/*
+ * Helper functions for managing events -- not part of the public interface.
+ * Props to Dean Edwards' addEvent library for many of the ideas.
+ */
+jQuery.event = {
+
+    global: {},
+
+    add: function( elem, types, handler, data, selector ) {
+
+        var handleObjIn, eventHandle, tmp,
+            events, t, handleObj,
+            special, handlers, type, namespaces, origType,
+            elemData = dataPriv.get( elem );
+
+        // Don't attach events to noData or text/comment nodes (but allow plain objects)
+        if ( !elemData ) {
+            return;
+        }
+
+        // Caller can pass in an object of custom data in lieu of the handler
+        if ( handler.handler ) {
+            handleObjIn = handler;
+            handler = handleObjIn.handler;
+            selector = handleObjIn.selector;
+        }
+
+        // Make sure that the handler has a unique ID, used to find/remove it later
+        if ( !handler.guid ) {
+            handler.guid = jQuery.guid++;
+        }
+
+        // Init the element's event structure and main handler, if this is the first
+        if ( !( events = elemData.events ) ) {
+            events = elemData.events = {};
+        }
+        if ( !( eventHandle = elemData.handle ) ) {
+            eventHandle = elemData.handle = function( e ) {
+
+                // Discard the second event of a jQuery.event.trigger() and
+                // when an event is called after a page has unloaded
+                return typeof jQuery !== "undefined" && jQuery.event.triggered !== e.type ?
+                    jQuery.event.dispatch.apply( elem, arguments ) : undefined;
+            };
+        }
+
+        // Handle multiple events separated by a space
+        types = ( types || "" ).match( rnotwhite ) || [ "" ];
+        t = types.length;
+        while ( t-- ) {
+            tmp = rtypenamespace.exec( types[ t ] ) || [];
+            type = origType = tmp[ 1 ];
+            namespaces = ( tmp[ 2 ] || "" ).split( "." ).sort();
+
+            // There *must* be a type, no attaching namespace-only handlers
+            if ( !type ) {
+                continue;
+            }
+
+            // If event changes its type, use the special event handlers for the changed type
+            special = jQuery.event.special[ type ] || {};
+
+            // If selector defined, determine special event api type, otherwise given type
+            type = ( selector ? special.delegateType : special.bindType ) || type;
+
+            // Update special based on newly reset type
+            special = jQuery.event.special[ type ] || {};
+
+            // handleObj is passed to all event handlers
+            handleObj = jQuery.extend( {
+                type: type,
+                origType: origType,
+                data: data,
+                handler: handler,
+                guid: handler.guid,
+                selector: selector,
+                needsContext: selector && jQuery.expr.match.needsContext.test( selector ),
+                namespace: namespaces.join( "." )
+            }, handleObjIn );
+
+            // Init the event handler queue if we're the first
+            if ( !( handlers = events[ type ] ) ) {
+                handlers = events[ type ] = [];
+                handlers.delegateCount = 0;
+
+                // Only use addEventListener if the special events handler returns false
+                if ( !special.setup ||
+                    special.setup.call( elem, data, namespaces, eventHandle ) === false ) {
+
+                    if ( elem.addEventListener ) {
+                        elem.addEventListener( type, eventHandle );
+                    }
+                }
+            }
+
+            if ( special.add ) {
+                special.add.call( elem, handleObj );
+
+                if ( !handleObj.handler.guid ) {
+                    handleObj.handler.guid = handler.guid;
+                }
+            }
+
+            // Add to the element's handler list, delegates in front
+            if ( selector ) {
+                handlers.splice( handlers.delegateCount++, 0, handleObj );
+            } else {
+                handlers.push( handleObj );
+            }
+
+            // Keep track of which events have ever been used, for event optimization
+            jQuery.event.global[ type ] = true;
+        }
+
+    },
+
+    // Detach an event or set of events from an element
+    remove: function( elem, types, handler, selector, mappedTypes ) {
+
+        var j, origCount, tmp,
+            events, t, handleObj,
+            special, handlers, type, namespaces, origType,
+            elemData = dataPriv.hasData( elem ) && dataPriv.get( elem );
+
+        if ( !elemData || !( events = elemData.events ) ) {
+            return;
+        }
+
+        // Once for each type.namespace in types; type may be omitted
+        types = ( types || "" ).match( rnotwhite ) || [ "" ];
+        t = types.length;
+        while ( t-- ) {
+            tmp = rtypenamespace.exec( types[ t ] ) || [];
+            type = origType = tmp[ 1 ];
+            namespaces = ( tmp[ 2 ] || "" ).split( "." ).sort();
+
+            // Unbind all events (on this namespace, if provided) for the element
+            if ( !type ) {
+                for ( type in events ) {
+                    jQuery.event.remove( elem, type + types[ t ], handler, selector, true );
+                }
+                continue;
+            }
+
+            special = jQuery.event.special[ type ] || {};
+            type = ( selector ? special.delegateType : special.bindType ) || type;
+            handlers = events[ type ] || [];
+            tmp = tmp[ 2 ] &&
+                new RegExp( "(^|\\.)" + namespaces.join( "\\.(?:.*\\.|)" ) + "(\\.|$)" );
+
+            // Remove matching events
+            origCount = j = handlers.length;
+            while ( j-- ) {
+                handleObj = handlers[ j ];
+
+                if ( ( mappedTypes || origType === handleObj.origType ) &&
+                    ( !handler || handler.guid === handleObj.guid ) &&
+                    ( !tmp || tmp.test( handleObj.namespace ) ) &&
+                    ( !selector || selector === handleObj.selector ||
+                        selector === "**" && handleObj.selector ) ) {
+                    handlers.splice( j, 1 );
+
+                    if ( handleObj.selector ) {
+                        handlers.delegateCount--;
+                    }
+                    if ( special.remove ) {
+                        special.remove.call( elem, handleObj );
+                    }
+                }
+            }
+
+            // Remove generic event handler if we removed something and no more handlers exist
+            // (avoids potential for endless recursion during removal of special event handlers)
+            if ( origCount && !handlers.length ) {
+                if ( !special.teardown ||
+                    special.teardown.call( elem, namespaces, elemData.handle ) === false ) {
+
+                    jQuery.removeEvent( elem, type, elemData.handle );
+                }
+
+                delete events[ type ];
+            }
+        }
+
+        // Remove data and the expando if it's no longer used
+        if ( jQuery.isEmptyObject( events ) ) {
+            dataPriv.remove( elem, "handle events" );
+        }
+    },
+
+    dispatch: function( event ) {
+
+        // Make a writable jQuery.Event from the native event object
+        event = jQuery.event.fix( event );
+
+        var i, j, ret, matched, handleObj,
+            handlerQueue = [],
+            args = slice.call( arguments ),
+            handlers = ( dataPriv.get( this, "events" ) || {} )[ event.type ] || [],
+            special = jQuery.event.special[ event.type ] || {};
+
+        // Use the fix-ed jQuery.Event rather than the (read-only) native event
+        args[ 0 ] = event;
+        event.delegateTarget = this;
+
+        // Call the preDispatch hook for the mapped type, and let it bail if desired
+        if ( special.preDispatch && special.preDispatch.call( this, event ) === false ) {
+            return;
+        }
+
+        // Determine handlers
+        handlerQueue = jQuery.event.handlers.call( this, event, handlers );
+
+        // Run delegates first; they may want to stop propagation beneath us
+        i = 0;
+        while ( ( matched = handlerQueue[ i++ ] ) && !event.isPropagationStopped() ) {
+            event.currentTarget = matched.elem;
+
+            j = 0;
+            while ( ( handleObj = matched.handlers[ j++ ] ) &&
+                !event.isImmediatePropagationStopped() ) {
+
+                // Triggered event must either 1) have no namespace, or 2) have namespace(s)
+                // a subset or equal to those in the bound event (both can have no namespace).
+                if ( !event.rnamespace || event.rnamespace.test( handleObj.namespace ) ) {
+
+                    event.handleObj = handleObj;
+                    event.data = handleObj.data;
+
+                    ret = ( ( jQuery.event.special[ handleObj.origType ] || {} ).handle ||
+                        handleObj.handler ).apply( matched.elem, args );
+
+                    if ( ret !== undefined ) {
+                        if ( ( event.result = ret ) === false ) {
+                            event.preventDefault();
+                            event.stopPropagation();
+                        }
+                    }
+                }
+            }
+        }
+
+        // Call the postDispatch hook for the mapped type
+        if ( special.postDispatch ) {
+            special.postDispatch.call( this, event );
+        }
+
+        return event.result;
+    },
+
+    handlers: function( event, handlers ) {
+        var i, matches, sel, handleObj,
+            handlerQueue = [],
+            delegateCount = handlers.delegateCount,
+            cur = event.target;
+
+        // Support (at least): Chrome, IE9
+        // Find delegate handlers
+        // Black-hole SVG <use> instance trees (#13180)
+        //
+        // Support: Firefox<=42+
+        // Avoid non-left-click in FF but don't block IE radio events (#3861, gh-2343)
+        if ( delegateCount && cur.nodeType &&
+            ( event.type !== "click" || isNaN( event.button ) || event.button < 1 ) ) {
+
+            for ( ; cur !== this; cur = cur.parentNode || this ) {
+
+                // Don't check non-elements (#13208)
+                // Don't process clicks on disabled elements (#6911, #8165, #11382, #11764)
+                if ( cur.nodeType === 1 && ( cur.disabled !== true || event.type !== "click" ) ) {
+                    matches = [];
+                    for ( i = 0; i < delegateCount; i++ ) {
+                        handleObj = handlers[ i ];
+
+                        // Don't conflict with Object.prototype properties (#13203)
+                        sel = handleObj.selector + " ";
+
+                        if ( matches[ sel ] === undefined ) {
+                            matches[ sel ] = handleObj.needsContext ?
+                                jQuery( sel, this ).index( cur ) > -1 :
+                                jQuery.find( sel, this, null, [ cur ] ).length;
+                        }
+                        if ( matches[ sel ] ) {
+                            matches.push( handleObj );
+                        }
+                    }
+                    if ( matches.length ) {
+                        handlerQueue.push( { elem: cur, handlers: matches } );
+                    }
+                }
+            }
+        }
+
+        // Add the remaining (directly-bound) handlers
+        if ( delegateCount < handlers.length ) {
+            handlerQueue.push( { elem: this, handlers: handlers.slice( delegateCount ) } );
+        }
+
+        return handlerQueue;
+    },
+
+    // Includes some event props shared by KeyEvent and MouseEvent
+    props: ( "altKey bubbles cancelable ctrlKey currentTarget detail eventPhase " +
+        "metaKey relatedTarget shiftKey target timeStamp view which" ).split( " " ),
+
+    fixHooks: {},
+
+    keyHooks: {
+        props: "char charCode key keyCode".split( " " ),
+        filter: function( event, original ) {
+
+            // Add which for key events
+            if ( event.which == null ) {
+                event.which = original.charCode != null ? original.charCode : original.keyCode;
+            }
+
+            return event;
+        }
+    },
+
+    mouseHooks: {
+        props: ( "button buttons clientX clientY offsetX offsetY pageX pageY " +
+            "screenX screenY toElement" ).split( " " ),
+        filter: function( event, original ) {
+            var eventDoc, doc, body,
+                button = original.button;
+
+            // Calculate pageX/Y if missing and clientX/Y available
+            if ( event.pageX == null && original.clientX != null ) {
+                eventDoc = event.target.ownerDocument || document;
+                doc = eventDoc.documentElement;
+                body = eventDoc.body;
+
+                event.pageX = original.clientX +
+                    ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) -
+                    ( doc && doc.clientLeft || body && body.clientLeft || 0 );
+                event.pageY = original.clientY +
+                    ( doc && doc.scrollTop  || body && body.scrollTop  || 0 ) -
+                    ( doc && doc.clientTop  || body && body.clientTop  || 0 );
+            }
+
+            // Add which for click: 1 === left; 2 === middle; 3 === right
+            // Note: button is not normalized, so don't use it
+            if ( !event.which && button !== undefined ) {
+                event.which = ( button & 1 ? 1 : ( button & 2 ? 3 : ( button & 4 ? 2 : 0 ) ) );
+            }
+
+            return event;
+        }
+    },
+
+    fix: function( event ) {
+        if ( event[ jQuery.expando ] ) {
+            return event;
+        }
+
+        // Create a writable copy of the event object and normalize some properties
+        var i, prop, copy,
+            type = event.type,
+            originalEvent = event,
+            fixHook = this.fixHooks[ type ];
+
+        if ( !fixHook ) {
+            this.fixHooks[ type ] = fixHook =
+                rmouseEvent.test( type ) ? this.mouseHooks :
+                rkeyEvent.test( type ) ? this.keyHooks :
+                {};
+        }
+        copy = fixHook.props ? this.props.concat( fixHook.props ) : this.props;
+
+        event = new jQuery.Event( originalEvent );
+
+        i = copy.length;
+        while ( i-- ) {
+            prop = copy[ i ];
+            event[ prop ] = originalEvent[ prop ];
+        }
+
+        // Support: Cordova 2.5 (WebKit) (#13255)
+        // All events should have a target; Cordova deviceready doesn't
+        if ( !event.target ) {
+            event.target = document;
+        }
+
+        // Support: Safari 6.0+, Chrome<28
+        // Target should not be a text node (#504, #13143)
+        if ( event.target.nodeType === 3 ) {
+            event.target = event.target.parentNode;
+        }
+
+        return fixHook.filter ? fixHook.filter( event, originalEvent ) : event;
+    },
+
+    special: {
+        load: {
+
+            // Prevent triggered image.load events from bubbling to window.load
+            noBubble: true
+        },
+        focus: {
+
+            // Fire native event if possible so blur/focus sequence is correct
+            trigger: function() {
+                if ( this !== safeActiveElement() && this.focus ) {
+                    this.focus();
+                    return false;
+                }
+            },
+            delegateType: "focusin"
+        },
+        blur: {
+            trigger: function() {
+                if ( this === safeActiveElement() && this.blur ) {
+                    this.blur();
+                    return false;
+                }
+            },
+            delegateType: "focusout"
+        },
+        click: {
+
+            // For checkbox, fire native event so checked state will be right
+            trigger: function() {
+                if ( this.type === "checkbox" && this.click && jQuery.nodeName( this, "input" ) ) {
+                    this.click();
+                    return false;
+                }
+            },
+
+            // For cross-browser consistency, don't fire native .click() on links
+            _default: function( event ) {
+                return jQuery.nodeName( event.target, "a" );
+            }
+        },
+
+        beforeunload: {
+            postDispatch: function( event ) {
+
+                // Support: Firefox 20+
+                // Firefox doesn't alert if the returnValue field is not set.
+                if ( event.result !== undefined && event.originalEvent ) {
+                    event.originalEvent.returnValue = event.result;
+                }
+            }
+        }
+    }
+};
+
+jQuery.removeEvent = function( elem, type, handle ) {
+
+    // This "if" is needed for plain objects
+    if ( elem.removeEventListener ) {
+        elem.removeEventListener( type, handle );
+    }
+};
+
+jQuery.Event = function( src, props ) {
+
+    // Allow instantiation without the 'new' keyword
+    if ( !( this instanceof jQuery.Event ) ) {
+        return new jQuery.Event( src, props );
+    }
+
+    // Event object
+    if ( src && src.type ) {
+        this.originalEvent = src;
+        this.type = src.type;
+
+        // Events bubbling up the document may have been marked as prevented
+        // by a handler lower down the tree; reflect the correct value.
+        this.isDefaultPrevented = src.defaultPrevented ||
+                src.defaultPrevented === undefined &&
+
+                // Support: Android<4.0
+                src.returnValue === false ?
+            returnTrue :
+            returnFalse;
+
+    // Event type
+    } else {
+        this.type = src;
+    }
+
+    // Put explicitly provided properties onto the event object
+    if ( props ) {
+        jQuery.extend( this, props );
+    }
+
+    // Create a timestamp if incoming event doesn't have one
+    this.timeStamp = src && src.timeStamp || jQuery.now();
+
+    // Mark it as fixed
+    this[ jQuery.expando ] = true;
+};
+
+// jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding
+// http://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html
+jQuery.Event.prototype = {
+    constructor: jQuery.Event,
+    isDefaultPrevented: returnFalse,
+    isPropagationStopped: returnFalse,
+    isImmediatePropagationStopped: returnFalse,
+    isSimulated: false,
+
+    preventDefault: function() {
+        var e = this.originalEvent;
+
+        this.isDefaultPrevented = returnTrue;
+
+        if ( e && !this.isSimulated ) {
+            e.preventDefault();
+        }
+    },
+    stopPropagation: function() {
+        var e = this.originalEvent;
+
+        this.isPropagationStopped = returnTrue;
+
+        if ( e && !this.isSimulated ) {
+            e.stopPropagation();
+        }
+    },
+    stopImmediatePropagation: function() {
+        var e = this.originalEvent;
+
+        this.isImmediatePropagationStopped = returnTrue;
+
+        if ( e && !this.isSimulated ) {
+            e.stopImmediatePropagation();
+        }
+
+        this.stopPropagation();
+    }
+};
+
+// Create mouseenter/leave events using mouseover/out and event-time checks
+// so that event delegation works in jQuery.
+// Do the same for pointerenter/pointerleave and pointerover/pointerout
+//
+// Support: Safari 7 only
+// Safari sends mouseenter too often; see:
+// https://code.google.com/p/chromium/issues/detail?id=470258
+// for the description of the bug (it existed in older Chrome versions as well).
+jQuery.each( {
+    mouseenter: "mouseover",
+    mouseleave: "mouseout",
+    pointerenter: "pointerover",
+    pointerleave: "pointerout"
+}, function( orig, fix ) {
+    jQuery.event.special[ orig ] = {
+        delegateType: fix,
+        bindType: fix,
+
+        handle: function( event ) {
+            var ret,
+                target = this,
+                related = event.relatedTarget,
+                handleObj = event.handleObj;
+
+            // For mouseenter/leave call the handler if related is outside the target.
+            // NB: No relatedTarget if the mouse left/entered the browser window
+            if ( !related || ( related !== target && !jQuery.contains( target, related ) ) ) {
+                event.type = handleObj.origType;
+                ret = handleObj.handler.apply( this, arguments );
+                event.type = fix;
+            }
+            return ret;
+        }
+    };
+} );
+
+jQuery.fn.extend( {
+    on: function( types, selector, data, fn ) {
+        return on( this, types, selector, data, fn );
+    },
+    one: function( types, selector, data, fn ) {
+        return on( this, types, selector, data, fn, 1 );
+    },
+    off: function( types, selector, fn ) {
+        var handleObj, type;
+        if ( types && types.preventDefault && types.handleObj ) {
+
+            // ( event )  dispatched jQuery.Event
+            handleObj = types.handleObj;
+            jQuery( types.delegateTarget ).off(
+                handleObj.namespace ?
+                    handleObj.origType + "." + handleObj.namespace :
+                    handleObj.origType,
+                handleObj.selector,
+                handleObj.handler
+            );
+            return this;
+        }
+        if ( typeof types === "object" ) {
+
+            // ( types-object [, selector] )
+            for ( type in types ) {
+                this.off( type, selector, types[ type ] );
+            }
+            return this;
+        }
+        if ( selector === false || typeof selector === "function" ) {
+
+            // ( types [, fn] )
+            fn = selector;
+            selector = undefined;
+        }
+        if ( fn === false ) {
+            fn = returnFalse;
+        }
+        return this.each( function() {
+            jQuery.event.remove( this, types, fn, selector );
+        } );
+    }
+} );
+
+
+var
+    rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:-]+)[^>]*)\/>/gi,
+
+    // Support: IE 10-11, Edge 10240+
+    // In IE/Edge using regex groups here causes severe slowdowns.
+    // See https://connect.microsoft.com/IE/feedback/details/1736512/
+    rnoInnerhtml = /<script|<style|<link/i,
+
+    // checked="checked" or checked
+    rchecked = /checked\s*(?:[^=]|=\s*.checked.)/i,
+    rscriptTypeMasked = /^true\/(.*)/,
+    rcleanScript = /^\s*<!(?:\[CDATA\[|--)|(?:\]\]|--)>\s*$/g;
+
+// Manipulating tables requires a tbody
+function manipulationTarget( elem, content ) {
+    return jQuery.nodeName( elem, "table" ) &&
+        jQuery.nodeName( content.nodeType !== 11 ? content : content.firstChild, "tr" ) ?
+
+        elem.getElementsByTagName( "tbody" )[ 0 ] ||
+            elem.appendChild( elem.ownerDocument.createElement( "tbody" ) ) :
+        elem;
+}
+
+// Replace/restore the type attribute of script elements for safe DOM manipulation
+function disableScript( elem ) {
+    elem.type = ( elem.getAttribute( "type" ) !== null ) + "/" + elem.type;
+    return elem;
+}
+function restoreScript( elem ) {
+    var match = rscriptTypeMasked.exec( elem.type );
+
+    if ( match ) {
+        elem.type = match[ 1 ];
+    } else {
+        elem.removeAttribute( "type" );
+    }
+
+    return elem;
+}
+
+function cloneCopyEvent( src, dest ) {
+    var i, l, type, pdataOld, pdataCur, udataOld, udataCur, events;
+
+    if ( dest.nodeType !== 1 ) {
+        return;
+    }
+
+    // 1. Copy private data: events, handlers, etc.
+    if ( dataPriv.hasData( src ) ) {
+        pdataOld = dataPriv.access( src );
+        pdataCur = dataPriv.set( dest, pdataOld );
+        events = pdataOld.events;
+
+        if ( events ) {
+            delete pdataCur.handle;
+            pdataCur.events = {};
+
+            for ( type in events ) {
+                for ( i = 0, l = events[ type ].length; i < l; i++ ) {
+                    jQuery.event.add( dest, type, events[ type ][ i ] );
+                }
+            }
+        }
+    }
+
+    // 2. Copy user data
+    if ( dataUser.hasData( src ) ) {
+        udataOld = dataUser.access( src );
+        udataCur = jQuery.extend( {}, udataOld );
+
+        dataUser.set( dest, udataCur );
+    }
+}
+
+// Fix IE bugs, see support tests
+function fixInput( src, dest ) {
+    var nodeName = dest.nodeName.toLowerCase();
+
+    // Fails to persist the checked state of a cloned checkbox or radio button.
+    if ( nodeName === "input" && rcheckableType.test( src.type ) ) {
+        dest.checked = src.checked;
+
+    // Fails to return the selected option to the default selected state when cloning options
+    } else if ( nodeName === "input" || nodeName === "textarea" ) {
+        dest.defaultValue = src.defaultValue;
+    }
+}
+
+function domManip( collection, args, callback, ignored ) {
+
+    // Flatten any nested arrays
+    args = concat.apply( [], args );
+
+    var fragment, first, scripts, hasScripts, node, doc,
+        i = 0,
+        l = collection.length,
+        iNoClone = l - 1,
+        value = args[ 0 ],
+        isFunction = jQuery.isFunction( value );
+
+    // We can't cloneNode fragments that contain checked, in WebKit
+    if ( isFunction ||
+            ( l > 1 && typeof value === "string" &&
+                !support.checkClone && rchecked.test( value ) ) ) {
+        return collection.each( function( index ) {
+            var self = collection.eq( index );
+            if ( isFunction ) {
+                args[ 0 ] = value.call( this, index, self.html() );
+            }
+            domManip( self, args, callback, ignored );
+        } );
+    }
+
+    if ( l ) {
+        fragment = buildFragment( args, collection[ 0 ].ownerDocument, false, collection, ignored );
+        first = fragment.firstChild;
+
+        if ( fragment.childNodes.length === 1 ) {
+            fragment = first;
+        }
+
+        // Require either new content or an interest in ignored elements to invoke the callback
+        if ( first || ignored ) {
+            scripts = jQuery.map( getAll( fragment, "script" ), disableScript );
+            hasScripts = scripts.length;
+
+            // Use the original fragment for the last item
+            // instead of the first because it can end up
+            // being emptied incorrectly in certain situations (#8070).
+            for ( ; i < l; i++ ) {
+                node = fragment;
+
+                if ( i !== iNoClone ) {
+                    node = jQuery.clone( node, true, true );
+
+                    // Keep references to cloned scripts for later restoration
+                    if ( hasScripts ) {
+
+                        // Support: Android<4.1, PhantomJS<2
+                        // push.apply(_, arraylike) throws on ancient WebKit
+                        jQuery.merge( scripts, getAll( node, "script" ) );
+                    }
+                }
+
+                callback.call( collection[ i ], node, i );
+            }
+
+            if ( hasScripts ) {
+                doc = scripts[ scripts.length - 1 ].ownerDocument;
+
+                // Reenable scripts
+                jQuery.map( scripts, restoreScript );
+
+                // Evaluate executable scripts on first document insertion
+                for ( i = 0; i < hasScripts; i++ ) {
+                    node = scripts[ i ];
+                    if ( rscriptType.test( node.type || "" ) &&
+                        !dataPriv.access( node, "globalEval" ) &&
+                        jQuery.contains( doc, node ) ) {
+
+                        if ( node.src ) {
+
+                            // Optional AJAX dependency, but won't run scripts if not present
+                            if ( jQuery._evalUrl ) {
+                                jQuery._evalUrl( node.src );
+                            }
+                        } else {
+                            jQuery.globalEval( node.textContent.replace( rcleanScript, "" ) );
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    return collection;
+}
+
+function remove( elem, selector, keepData ) {
+    var node,
+        nodes = selector ? jQuery.filter( selector, elem ) : elem,
+        i = 0;
+
+    for ( ; ( node = nodes[ i ] ) != null; i++ ) {
+        if ( !keepData && node.nodeType === 1 ) {
+            jQuery.cleanData( getAll( node ) );
+        }
+
+        if ( node.parentNode ) {
+            if ( keepData && jQuery.contains( node.ownerDocument, node ) ) {
+                setGlobalEval( getAll( node, "script" ) );
+            }
+            node.parentNode.removeChild( node );
+        }
+    }
+
+    return elem;
+}
+
+jQuery.extend( {
+    htmlPrefilter: function( html ) {
+        return html.replace( rxhtmlTag, "<$1></$2>" );
+    },
+
+    clone: function( elem, dataAndEvents, deepDataAndEvents ) {
+        var i, l, srcElements, destElements,
+            clone = elem.cloneNode( true ),
+            inPage = jQuery.contains( elem.ownerDocument, elem );
+
+        // Fix IE cloning issues
+        if ( !support.noCloneChecked && ( elem.nodeType === 1 || elem.nodeType === 11 ) &&
+                !jQuery.isXMLDoc( elem ) ) {
+
+            // We eschew Sizzle here for performance reasons: http://jsperf.com/getall-vs-sizzle/2
+            destElements = getAll( clone );
+            srcElements = getAll( elem );
+
+            for ( i = 0, l = srcElements.length; i < l; i++ ) {
+                fixInput( srcElements[ i ], destElements[ i ] );
+            }
+        }
+
+        // Copy the events from the original to the clone
+        if ( dataAndEvents ) {
+            if ( deepDataAndEvents ) {
+                srcElements = srcElements || getAll( elem );
+                destElements = destElements || getAll( clone );
+
+                for ( i = 0, l = srcElements.length; i < l; i++ ) {
+                    cloneCopyEvent( srcElements[ i ], destElements[ i ] );
+                }
+            } else {
+                cloneCopyEvent( elem, clone );
+            }
+        }
+
+        // Preserve script evaluation history
+        destElements = getAll( clone, "script" );
+        if ( destElements.length > 0 ) {
+            setGlobalEval( destElements, !inPage && getAll( elem, "script" ) );
+        }
+
+        // Return the cloned set
+        return clone;
+    },
+
+    cleanData: function( elems ) {
+        var data, elem, type,
+            special = jQuery.event.special,
+            i = 0;
+
+        for ( ; ( elem = elems[ i ] ) !== undefined; i++ ) {
+            if ( acceptData( elem ) ) {
+                if ( ( data = elem[ dataPriv.expando ] ) ) {
+                    if ( data.events ) {
+                        for ( type in data.events ) {
+                            if ( special[ type ] ) {
+                                jQuery.event.remove( elem, type );
+
+                            // This is a shortcut to avoid jQuery.event.remove's overhead
+                            } else {
+                                jQuery.removeEvent( elem, type, data.handle );
+                            }
+                        }
+                    }
+
+                    // Support: Chrome <= 35-45+
+                    // Assign undefined instead of using delete, see Data#remove
+                    elem[ dataPriv.expando ] = undefined;
+                }
+                if ( elem[ dataUser.expando ] ) {
+
+                    // Support: Chrome <= 35-45+
+                    // Assign undefined instead of using delete, see Data#remove
+                    elem[ dataUser.expando ] = undefined;
+                }
+            }
+        }
+    }
+} );
+
+jQuery.fn.extend( {
+
+    // Keep domManip exposed until 3.0 (gh-2225)
+    domManip: domManip,
+
+    detach: function( selector ) {
+        return remove( this, selector, true );
+    },
+
+    remove: function( selector ) {
+        return remove( this, selector );
+    },
+
+    text: function( value ) {
+        return access( this, function( value ) {
+            return value === undefined ?
+                jQuery.text( this ) :
+                this.empty().each( function() {
+                    if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) {
+                        this.textContent = value;
+                    }
+                } );
+        }, null, value, arguments.length );
+    },
+
+    append: function() {
+        return domManip( this, arguments, function( elem ) {
+            if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) {
+                var target = manipulationTarget( this, elem );
+                target.appendChild( elem );
+            }
+        } );
+    },
+
+    prepend: function() {
+        return domManip( this, arguments, function( elem ) {
+            if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) {
+                var target = manipulationTarget( this, elem );
+                target.insertBefore( elem, target.firstChild );
+            }
+        } );
+    },
+
+    before: function() {
+        return domManip( this, arguments, function( elem ) {
+            if ( this.parentNode ) {
+                this.parentNode.insertBefore( elem, this );
+            }
+        } );
+    },
+
+    after: function() {
+        return domManip( this, arguments, function( elem ) {
+            if ( this.parentNode ) {
+                this.parentNode.insertBefore( elem, this.nextSibling );
+            }
+        } );
+    },
+
+    empty: function() {
+        var elem,
+            i = 0;
+
+        for ( ; ( elem = this[ i ] ) != null; i++ ) {
+            if ( elem.nodeType === 1 ) {
+
+                // Prevent memory leaks
+                jQuery.cleanData( getAll( elem, false ) );
+
+                // Remove any remaining nodes
+                elem.textContent = "";
+            }
+        }
+
+        return this;
+    },
+
+    clone: function( dataAndEvents, deepDataAndEvents ) {
+        dataAndEvents = dataAndEvents == null ? false : dataAndEvents;
+        deepDataAndEvents = deepDataAndEvents == null ? dataAndEvents : deepDataAndEvents;
+
+        return this.map( function() {
+            return jQuery.clone( this, dataAndEvents, deepDataAndEvents );
+        } );
+    },
+
+    html: function( value ) {
+        return access( this, function( value ) {
+            var elem = this[ 0 ] || {},
+                i = 0,
+                l = this.length;
+
+            if ( value === undefined && elem.nodeType === 1 ) {
+                return elem.innerHTML;
+            }
+
+            // See if we can take a shortcut and just use innerHTML
+            if ( typeof value === "string" && !rnoInnerhtml.test( value ) &&
+                !wrapMap[ ( rtagName.exec( value ) || [ "", "" ] )[ 1 ].toLowerCase() ] ) {
+
+                value = jQuery.htmlPrefilter( value );
+
+                try {
+                    for ( ; i < l; i++ ) {
+                        elem = this[ i ] || {};
+
+                        // Remove element nodes and prevent memory leaks
+                        if ( elem.nodeType === 1 ) {
+                            jQuery.cleanData( getAll( elem, false ) );
+                            elem.innerHTML = value;
+                        }
+                    }
+
+                    elem = 0;
+
+                // If using innerHTML throws an exception, use the fallback method
+                } catch ( e ) {}
+            }
+
+            if ( elem ) {
+                this.empty().append( value );
+            }
+        }, null, value, arguments.length );
+    },
+
+    replaceWith: function() {
+        var ignored = [];
+
+        // Make the changes, replacing each non-ignored context element with the new content
+        return domManip( this, arguments, function( elem ) {
+            var parent = this.parentNode;
+
+            if ( jQuery.inArray( this, ignored ) < 0 ) {
+                jQuery.cleanData( getAll( this ) );
+                if ( parent ) {
+                    parent.replaceChild( elem, this );
+                }
+            }
+
+        // Force callback invocation
+        }, ignored );
+    }
+} );
+
+jQuery.each( {
+    appendTo: "append",
+    prependTo: "prepend",
+    insertBefore: "before",
+    insertAfter: "after",
+    replaceAll: "replaceWith"
+}, function( name, original ) {
+    jQuery.fn[ name ] = function( selector ) {
+        var elems,
+            ret = [],
+            insert = jQuery( selector ),
+            last = insert.length - 1,
+            i = 0;
+
+        for ( ; i <= last; i++ ) {
+            elems = i === last ? this : this.clone( true );
+            jQuery( insert[ i ] )[ original ]( elems );
+
+            // Support: QtWebKit
+            // .get() because push.apply(_, arraylike) throws
+            push.apply( ret, elems.get() );
+        }
+
+        return this.pushStack( ret );
+    };
+} );
+
+
+var iframe,
+    elemdisplay = {
+
+        // Support: Firefox
+        // We have to pre-define these values for FF (#10227)
+        HTML: "block",
+        BODY: "block"
+    };
+
+/**
+ * Retrieve the actual display of a element
+ * @param {String} name nodeName of the element
+ * @param {Object} doc Document object
+ */
+
+// Called only from within defaultDisplay
+function actualDisplay( name, doc ) {
+    var elem = jQuery( doc.createElement( name ) ).appendTo( doc.body ),
+
+        display = jQuery.css( elem[ 0 ], "display" );
+
+    // We don't have any data stored on the element,
+    // so use "detach" method as fast way to get rid of the element
+    elem.detach();
+
+    return display;
+}
+
+/**
+ * Try to determine the default display value of an element
+ * @param {String} nodeName
+ */
+function defaultDisplay( nodeName ) {
+    var doc = document,
+        display = elemdisplay[ nodeName ];
+
+    if ( !display ) {
+        display = actualDisplay( nodeName, doc );
+
+        // If the simple way fails, read from inside an iframe
+        if ( display === "none" || !display ) {
+
+            // Use the already-created iframe if possible
+            iframe = ( iframe || jQuery( "<iframe frameborder='0' width='0' height='0'/>" ) )
+                .appendTo( doc.documentElement );
+
+            // Always write a new HTML skeleton so Webkit and Firefox don't choke on reuse
+            doc = iframe[ 0 ].contentDocument;
+
+            // Support: IE
+            doc.write();
+            doc.close();
+
+            display = actualDisplay( nodeName, doc );
+            iframe.detach();
+        }
+
+        // Store the correct default display
+        elemdisplay[ nodeName ] = display;
+    }
+
+    return display;
+}
+var rmargin = ( /^margin/ );
+
+var rnumnonpx = new RegExp( "^(" + pnum + ")(?!px)[a-z%]+$", "i" );
+
+var getStyles = function( elem ) {
+
+        // Support: IE<=11+, Firefox<=30+ (#15098, #14150)
+        // IE throws on elements created in popups
+        // FF meanwhile throws on frame elements through "defaultView.getComputedStyle"
+        var view = elem.ownerDocument.defaultView;
+
+        if ( !view || !view.opener ) {
+            view = window;
+        }
+
+        return view.getComputedStyle( elem );
+    };
+
+var swap = function( elem, options, callback, args ) {
+    var ret, name,
+        old = {};
+
+    // Remember the old values, and insert the new ones
+    for ( name in options ) {
+        old[ name ] = elem.style[ name ];
+        elem.style[ name ] = options[ name ];
+    }
+
+    ret = callback.apply( elem, args || [] );
+
+    // Revert the old values
+    for ( name in options ) {
+        elem.style[ name ] = old[ name ];
+    }
+
+    return ret;
+};
+
+
+var documentElement = document.documentElement;
+
+
+
+( function() {
+    var pixelPositionVal, boxSizingReliableVal, pixelMarginRightVal, reliableMarginLeftVal,
+        container = document.createElement( "div" ),
+        div = document.createElement( "div" );
+
+    // Finish early in limited (non-browser) environments
+    if ( !div.style ) {
+        return;
+    }
+
+    // Support: IE9-11+
+    // Style of cloned element affects source element cloned (#8908)
+    div.style.backgroundClip = "content-box";
+    div.cloneNode( true ).style.backgroundClip = "";
+    support.clearCloneStyle = div.style.backgroundClip === "content-box";
+
+    container.style.cssText = "border:0;width:8px;height:0;top:0;left:-9999px;" +
+        "padding:0;margin-top:1px;position:absolute";
+    container.appendChild( div );
+
+    // Executing both pixelPosition & boxSizingReliable tests require only one layout
+    // so they're executed at the same time to save the second computation.
+    function computeStyleTests() {
+        div.style.cssText =
+
+            // Support: Firefox<29, Android 2.3
+            // Vendor-prefix box-sizing
+            "-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;" +
+            "position:relative;display:block;" +
+            "margin:auto;border:1px;padding:1px;" +
+            "top:1%;width:50%";
+        div.innerHTML = "";
+        documentElement.appendChild( container );
+
+        var divStyle = window.getComputedStyle( div );
+        pixelPositionVal = divStyle.top !== "1%";
+        reliableMarginLeftVal = divStyle.marginLeft === "2px";
+        boxSizingReliableVal = divStyle.width === "4px";
+
+        // Support: Android 4.0 - 4.3 only
+        // Some styles come back with percentage values, even though they shouldn't
+        div.style.marginRight = "50%";
+        pixelMarginRightVal = divStyle.marginRight === "4px";
+
+        documentElement.removeChild( container );
+    }
+
+    jQuery.extend( support, {
+        pixelPosition: function() {
+
+            // This test is executed only once but we still do memoizing
+            // since we can use the boxSizingReliable pre-computing.
+            // No need to check if the test was already performed, though.
+            computeStyleTests();
+            return pixelPositionVal;
+        },
+        boxSizingReliable: function() {
+            if ( boxSizingReliableVal == null ) {
+                computeStyleTests();
+            }
+            return boxSizingReliableVal;
+        },
+        pixelMarginRight: function() {
+
+            // Support: Android 4.0-4.3
+            // We're checking for boxSizingReliableVal here instead of pixelMarginRightVal
+            // since that compresses better and they're computed together anyway.
+            if ( boxSizingReliableVal == null ) {
+                computeStyleTests();
+            }
+            return pixelMarginRightVal;
+        },
+        reliableMarginLeft: function() {
+
+            // Support: IE <=8 only, Android 4.0 - 4.3 only, Firefox <=3 - 37
+            if ( boxSizingReliableVal == null ) {
+                computeStyleTests();
+            }
+            return reliableMarginLeftVal;
+        },
+        reliableMarginRight: function() {
+
+            // Support: Android 2.3
+            // Check if div with explicit width and no margin-right incorrectly
+            // gets computed margin-right based on width of container. (#3333)
+            // WebKit Bug 13343 - getComputedStyle returns wrong value for margin-right
+            // This support function is only executed once so no memoizing is needed.
+            var ret,
+                marginDiv = div.appendChild( document.createElement( "div" ) );
+
+            // Reset CSS: box-sizing; display; margin; border; padding
+            marginDiv.style.cssText = div.style.cssText =
+
+                // Support: Android 2.3
+                // Vendor-prefix box-sizing
+                "-webkit-box-sizing:content-box;box-sizing:content-box;" +
+                "display:block;margin:0;border:0;padding:0";
+            marginDiv.style.marginRight = marginDiv.style.width = "0";
+            div.style.width = "1px";
+            documentElement.appendChild( container );
+
+            ret = !parseFloat( window.getComputedStyle( marginDiv ).marginRight );
+
+            documentElement.removeChild( container );
+            div.removeChild( marginDiv );
+
+            return ret;
+        }
+    } );
+} )();
+
+
+function curCSS( elem, name, computed ) {
+    var width, minWidth, maxWidth, ret,
+        style = elem.style;
+
+    computed = computed || getStyles( elem );
+    ret = computed ? computed.getPropertyValue( name ) || computed[ name ] : undefined;
+
+    // Support: Opera 12.1x only
+    // Fall back to style even without computed
+    // computed is undefined for elems on document fragments
+    if ( ( ret === "" || ret === undefined ) && !jQuery.contains( elem.ownerDocument, elem ) ) {
+        ret = jQuery.style( elem, name );
+    }
+
+    // Support: IE9
+    // getPropertyValue is only needed for .css('filter') (#12537)
+    if ( computed ) {
+
+        // A tribute to the "awesome hack by Dean Edwards"
+        // Android Browser returns percentage for some values,
+        // but width seems to be reliably pixels.
+        // This is against the CSSOM draft spec:
+        // http://dev.w3.org/csswg/cssom/#resolved-values
+        if ( !support.pixelMarginRight() && rnumnonpx.test( ret ) && rmargin.test( name ) ) {
+
+            // Remember the original values
+            width = style.width;
+            minWidth = style.minWidth;
+            maxWidth = style.maxWidth;
+
+            // Put in the new values to get a computed value out
+            style.minWidth = style.maxWidth = style.width = ret;
+            ret = computed.width;
+
+            // Revert the changed values
+            style.width = width;
+            style.minWidth = minWidth;
+            style.maxWidth = maxWidth;
+        }
+    }
+
+    return ret !== undefined ?
+
+        // Support: IE9-11+
+        // IE returns zIndex value as an integer.
+        ret + "" :
+        ret;
+}
+
+
+function addGetHookIf( conditionFn, hookFn ) {
+
+    // Define the hook, we'll check on the first run if it's really needed.
+    return {
+        get: function() {
+            if ( conditionFn() ) {
+
+                // Hook not needed (or it's not possible to use it due
+                // to missing dependency), remove it.
+                delete this.get;
+                return;
+            }
+
+            // Hook needed; redefine it so that the support test is not executed again.
+            return ( this.get = hookFn ).apply( this, arguments );
+        }
+    };
+}
+
+
+var
+
+    // Swappable if display is none or starts with table
+    // except "table", "table-cell", or "table-caption"
+    // See here for display values: https://developer.mozilla.org/en-US/docs/CSS/display
+    rdisplayswap = /^(none|table(?!-c[ea]).+)/,
+
+    cssShow = { position: "absolute", visibility: "hidden", display: "block" },
+    cssNormalTransform = {
+        letterSpacing: "0",
+        fontWeight: "400"
+    },
+
+    cssPrefixes = [ "Webkit", "O", "Moz", "ms" ],
+    emptyStyle = document.createElement( "div" ).style;
+
+// Return a css property mapped to a potentially vendor prefixed property
+function vendorPropName( name ) {
+
+    // Shortcut for names that are not vendor prefixed
+    if ( name in emptyStyle ) {
+        return name;
+    }
+
+    // Check for vendor prefixed names
+    var capName = name[ 0 ].toUpperCase() + name.slice( 1 ),
+        i = cssPrefixes.length;
+
+    while ( i-- ) {
+        name = cssPrefixes[ i ] + capName;
+        if ( name in emptyStyle ) {
+            return name;
+        }
+    }
+}
+
+function setPositiveNumber( elem, value, subtract ) {
+
+    // Any relative (+/-) values have already been
+    // normalized at this point
+    var matches = rcssNum.exec( value );
+    return matches ?
+
+        // Guard against undefined "subtract", e.g., when used as in cssHooks
+        Math.max( 0, matches[ 2 ] - ( subtract || 0 ) ) + ( matches[ 3 ] || "px" ) :
+        value;
+}
+
+function augmentWidthOrHeight( elem, name, extra, isBorderBox, styles ) {
+    var i = extra === ( isBorderBox ? "border" : "content" ) ?
+
+        // If we already have the right measurement, avoid augmentation
+        4 :
+
+        // Otherwise initialize for horizontal or vertical properties
+        name === "width" ? 1 : 0,
+
+        val = 0;
+
+    for ( ; i < 4; i += 2 ) {
+
+        // Both box models exclude margin, so add it if we want it
+        if ( extra === "margin" ) {
+            val += jQuery.css( elem, extra + cssExpand[ i ], true, styles );
+        }
+
+        if ( isBorderBox ) {
+
+            // border-box includes padding, so remove it if we want content
+            if ( extra === "content" ) {
+                val -= jQuery.css( elem, "padding" + cssExpand[ i ], true, styles );
+            }
+
+            // At this point, extra isn't border nor margin, so remove border
+            if ( extra !== "margin" ) {
+                val -= jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles );
+            }
+        } else {
+
+            // At this point, extra isn't content, so add padding
+            val += jQuery.css( elem, "padding" + cssExpand[ i ], true, styles );
+
+            // At this point, extra isn't content nor padding, so add border
+            if ( extra !== "padding" ) {
+                val += jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles );
+            }
+        }
+    }
+
+    return val;
+}
+
+function getWidthOrHeight( elem, name, extra ) {
+
+    // Start with offset property, which is equivalent to the border-box value
+    var valueIsBorderBox = true,
+        val = name === "width" ? elem.offsetWidth : elem.offsetHeight,
+        styles = getStyles( elem ),
+        isBorderBox = jQuery.css( elem, "boxSizing", false, styles ) === "border-box";
+
+    // Some non-html elements return undefined for offsetWidth, so check for null/undefined
+    // svg - https://bugzilla.mozilla.org/show_bug.cgi?id=649285
+    // MathML - https://bugzilla.mozilla.org/show_bug.cgi?id=491668
+    if ( val <= 0 || val == null ) {
+
+        // Fall back to computed then uncomputed css if necessary
+        val = curCSS( elem, name, styles );
+        if ( val < 0 || val == null ) {
+            val = elem.style[ name ];
+        }
+
+        // Computed unit is not pixels. Stop here and return.
+        if ( rnumnonpx.test( val ) ) {
+            return val;
+        }
+
+        // Check for style in case a browser which returns unreliable values
+        // for getComputedStyle silently falls back to the reliable elem.style
+        valueIsBorderBox = isBorderBox &&
+            ( support.boxSizingReliable() || val === elem.style[ name ] );
+
+        // Normalize "", auto, and prepare for extra
+        val = parseFloat( val ) || 0;
+    }
+
+    // Use the active box-sizing model to add/subtract irrelevant styles
+    return ( val +
+        augmentWidthOrHeight(
+            elem,
+            name,
+            extra || ( isBorderBox ? "border" : "content" ),
+            valueIsBorderBox,
+            styles
+        )
+    ) + "px";
+}
+
+function showHide( elements, show ) {
+    var display, elem, hidden,
+        values = [],
+        index = 0,
+        length = elements.length;
+
+    for ( ; index < length; index++ ) {
+        elem = elements[ index ];
+        if ( !elem.style ) {
+            continue;
+        }
+
+        values[ index ] = dataPriv.get( elem, "olddisplay" );
+        display = elem.style.display;
+        if ( show ) {
+
+            // Reset the inline display of this element to learn if it is
+            // being hidden by cascaded rules or not
+            if ( !values[ index ] && display === "none" ) {
+                elem.style.display = "";
+            }
+
+            // Set elements which have been overridden with display: none
+            // in a stylesheet to whatever the default browser style is
+            // for such an element
+            if ( elem.style.display === "" && isHidden( elem ) ) {
+                values[ index ] = dataPriv.access(
+                    elem,
+                    "olddisplay",
+                    defaultDisplay( elem.nodeName )
+                );
+            }
+        } else {
+            hidden = isHidden( elem );
+
+            if ( display !== "none" || !hidden ) {
+                dataPriv.set(
+                    elem,
+                    "olddisplay",
+                    hidden ? display : jQuery.css( elem, "display" )
+                );
+            }
+        }
+    }
+
+    // Set the display of most of the elements in a second loop
+    // to avoid the constant reflow
+    for ( index = 0; index < length; index++ ) {
+        elem = elements[ index ];
+        if ( !elem.style ) {
+            continue;
+        }
+        if ( !show || elem.style.display === "none" || elem.style.display === "" ) {
+            elem.style.display = show ? values[ index ] || "" : "none";
+        }
+    }
+
+    return elements;
+}
+
+jQuery.extend( {
+
+    // Add in style property hooks for overriding the default
+    // behavior of getting and setting a style property
+    cssHooks: {
+        opacity: {
+            get: function( elem, computed ) {
+                if ( computed ) {
+
+                    // We should always get a number back from opacity
+                    var ret = curCSS( elem, "opacity" );
+                    return ret === "" ? "1" : ret;
+                }
+            }
+        }
+    },
+
+    // Don't automatically add "px" to these possibly-unitless properties
+    cssNumber: {
+        "animationIterationCount": true,
+        "columnCount": true,
+        "fillOpacity": true,
+        "flexGrow": true,
+        "flexShrink": true,
+        "fontWeight": true,
+        "lineHeight": true,
+        "opacity": true,
+        "order": true,
+        "orphans": true,
+        "widows": true,
+        "zIndex": true,
+        "zoom": true
+    },
+
+    // Add in properties whose names you wish to fix before
+    // setting or getting the value
+    cssProps: {
+        "float": "cssFloat"
+    },
+
+    // Get and set the style property on a DOM Node
+    style: function( elem, name, value, extra ) {
+
+        // Don't set styles on text and comment nodes
+        if ( !elem || elem.nodeType === 3 || elem.nodeType === 8 || !elem.style ) {
+            return;
+        }
+
+        // Make sure that we're working with the right name
+        var ret, type, hooks,
+            origName = jQuery.camelCase( name ),
+            style = elem.style;
+
+        name = jQuery.cssProps[ origName ] ||
+            ( jQuery.cssProps[ origName ] = vendorPropName( origName ) || origName );
+
+        // Gets hook for the prefixed version, then unprefixed version
+        hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ];
+
+        // Check if we're setting a value
+        if ( value !== undefined ) {
+            type = typeof value;
+
+            // Convert "+=" or "-=" to relative numbers (#7345)
+            if ( type === "string" && ( ret = rcssNum.exec( value ) ) && ret[ 1 ] ) {
+                value = adjustCSS( elem, name, ret );
+
+                // Fixes bug #9237
+                type = "number";
+            }
+
+            // Make sure that null and NaN values aren't set (#7116)
+            if ( value == null || value !== value ) {
+                return;
+            }
+
+            // If a number was passed in, add the unit (except for certain CSS properties)
+            if ( type === "number" ) {
+                value += ret && ret[ 3 ] || ( jQuery.cssNumber[ origName ] ? "" : "px" );
+            }
+
+            // Support: IE9-11+
+            // background-* props affect original clone's values
+            if ( !support.clearCloneStyle && value === "" && name.indexOf( "background" ) === 0 ) {
+                style[ name ] = "inherit";
+            }
+
+            // If a hook was provided, use that value, otherwise just set the specified value
+            if ( !hooks || !( "set" in hooks ) ||
+                ( value = hooks.set( elem, value, extra ) ) !== undefined ) {
+
+                style[ name ] = value;
+            }
+
+        } else {
+
+            // If a hook was provided get the non-computed value from there
+            if ( hooks && "get" in hooks &&
+                ( ret = hooks.get( elem, false, extra ) ) !== undefined ) {
+
+                return ret;
+            }
+
+            // Otherwise just get the value from the style object
+            return style[ name ];
+        }
+    },
+
+    css: function( elem, name, extra, styles ) {
+        var val, num, hooks,
+            origName = jQuery.camelCase( name );
+
+        // Make sure that we're working with the right name
+        name = jQuery.cssProps[ origName ] ||
+            ( jQuery.cssProps[ origName ] = vendorPropName( origName ) || origName );
+
+        // Try prefixed name followed by the unprefixed name
+        hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ];
+
+        // If a hook was provided get the computed value from there
+        if ( hooks && "get" in hooks ) {
+            val = hooks.get( elem, true, extra );
+        }
+
+        // Otherwise, if a way to get the computed value exists, use that
+        if ( val === undefined ) {
+            val = curCSS( elem, name, styles );
+        }
+
+        // Convert "normal" to computed value
+        if ( val === "normal" && name in cssNormalTransform ) {
+            val = cssNormalTransform[ name ];
+        }
+
+        // Make numeric if forced or a qualifier was provided and val looks numeric
+        if ( extra === "" || extra ) {
+            num = parseFloat( val );
+            return extra === true || isFinite( num ) ? num || 0 : val;
+        }
+        return val;
+    }
+} );
+
+jQuery.each( [ "height", "width" ], function( i, name ) {
+    jQuery.cssHooks[ name ] = {
+        get: function( elem, computed, extra ) {
+            if ( computed ) {
+
+                // Certain elements can have dimension info if we invisibly show them
+                // but it must have a current display style that would benefit
+                return rdisplayswap.test( jQuery.css( elem, "display" ) ) &&
+                    elem.offsetWidth === 0 ?
+                        swap( elem, cssShow, function() {
+                            return getWidthOrHeight( elem, name, extra );
+                        } ) :
+                        getWidthOrHeight( elem, name, extra );
+            }
+        },
+
+        set: function( elem, value, extra ) {
+            var matches,
+                styles = extra && getStyles( elem ),
+                subtract = extra && augmentWidthOrHeight(
+                    elem,
+                    name,
+                    extra,
+                    jQuery.css( elem, "boxSizing", false, styles ) === "border-box",
+                    styles
+                );
+
+            // Convert to pixels if value adjustment is needed
+            if ( subtract && ( matches = rcssNum.exec( value ) ) &&
+                ( matches[ 3 ] || "px" ) !== "px" ) {
+
+                elem.style[ name ] = value;
+                value = jQuery.css( elem, name );
+            }
+
+            return setPositiveNumber( elem, value, subtract );
+        }
+    };
+} );
+
+jQuery.cssHooks.marginLeft = addGetHookIf( support.reliableMarginLeft,
+    function( elem, computed ) {
+        if ( computed ) {
+            return ( parseFloat( curCSS( elem, "marginLeft" ) ) ||
+                elem.getBoundingClientRect().left -
+                    swap( elem, { marginLeft: 0 }, function() {
+                        return elem.getBoundingClientRect().left;
+                    } )
+                ) + "px";
+        }
+    }
+);
+
+// Support: Android 2.3
+jQuery.cssHooks.marginRight = addGetHookIf( support.reliableMarginRight,
+    function( elem, computed ) {
+        if ( computed ) {
+            return swap( elem, { "display": "inline-block" },
+                curCSS, [ elem, "marginRight" ] );
+        }
+    }
+);
+
+// These hooks are used by animate to expand properties
+jQuery.each( {
+    margin: "",
+    padding: "",
+    border: "Width"
+}, function( prefix, suffix ) {
+    jQuery.cssHooks[ prefix + suffix ] = {
+        expand: function( value ) {
+            var i = 0,
+                expanded = {},
+
+                // Assumes a single number if not a string
+                parts = typeof value === "string" ? value.split( " " ) : [ value ];
+
+            for ( ; i < 4; i++ ) {
+                expanded[ prefix + cssExpand[ i ] + suffix ] =
+                    parts[ i ] || parts[ i - 2 ] || parts[ 0 ];
+            }
+
+            return expanded;
+        }
+    };
+
+    if ( !rmargin.test( prefix ) ) {
+        jQuery.cssHooks[ prefix + suffix ].set = setPositiveNumber;
+    }
+} );
+
+jQuery.fn.extend( {
+    css: function( name, value ) {
+        return access( this, function( elem, name, value ) {
+            var styles, len,
+                map = {},
+                i = 0;
+
+            if ( jQuery.isArray( name ) ) {
+                styles = getStyles( elem );
+                len = name.length;
+
+                for ( ; i < len; i++ ) {
+                    map[ name[ i ] ] = jQuery.css( elem, name[ i ], false, styles );
+                }
+
+                return map;
+            }
+
+            return value !== undefined ?
+                jQuery.style( elem, name, value ) :
+                jQuery.css( elem, name );
+        }, name, value, arguments.length > 1 );
+    },
+    show: function() {
+        return showHide( this, true );
+    },
+    hide: function() {
+        return showHide( this );
+    },
+    toggle: function( state ) {
+        if ( typeof state === "boolean" ) {
+            return state ? this.show() : this.hide();
+        }
+
+        return this.each( function() {
+            if ( isHidden( this ) ) {
+                jQuery( this ).show();
+            } else {
+                jQuery( this ).hide();
+            }
+        } );
+    }
+} );
+
+
+function Tween( elem, options, prop, end, easing ) {
+    return new Tween.prototype.init( elem, options, prop, end, easing );
+}
+jQuery.Tween = Tween;
+
+Tween.prototype = {
+    constructor: Tween,
+    init: function( elem, options, prop, end, easing, unit ) {
+        this.elem = elem;
+        this.prop = prop;
+        this.easing = easing || jQuery.easing._default;
+        this.options = options;
+        this.start = this.now = this.cur();
+        this.end = end;
+        this.unit = unit || ( jQuery.cssNumber[ prop ] ? "" : "px" );
+    },
+    cur: function() {
+        var hooks = Tween.propHooks[ this.prop ];
+
+        return hooks && hooks.get ?
+            hooks.get( this ) :
+            Tween.propHooks._default.get( this );
+    },
+    run: function( percent ) {
+        var eased,
+            hooks = Tween.propHooks[ this.prop ];
+
+        if ( this.options.duration ) {
+            this.pos = eased = jQuery.easing[ this.easing ](
+                percent, this.options.duration * percent, 0, 1, this.options.duration
+            );
+        } else {
+            this.pos = eased = percent;
+        }
+        this.now = ( this.end - this.start ) * eased + this.start;
+
+        if ( this.options.step ) {
+            this.options.step.call( this.elem, this.now, this );
+        }
+
+        if ( hooks && hooks.set ) {
+            hooks.set( this );
+        } else {
+            Tween.propHooks._default.set( this );
+        }
+        return this;
+    }
+};
+
+Tween.prototype.init.prototype = Tween.prototype;
+
+Tween.propHooks = {
+    _default: {
+        get: function( tween ) {
+            var result;
+
+            // Use a property on the element directly when it is not a DOM element,
+            // or when there is no matching style property that exists.
+            if ( tween.elem.nodeType !== 1 ||
+                tween.elem[ tween.prop ] != null && tween.elem.style[ tween.prop ] == null ) {
+                return tween.elem[ tween.prop ];
+            }
+
+            // Passing an empty string as a 3rd parameter to .css will automatically
+            // attempt a parseFloat and fallback to a string if the parse fails.
+            // Simple values such as "10px" are parsed to Float;
+            // complex values such as "rotate(1rad)" are returned as-is.
+            result = jQuery.css( tween.elem, tween.prop, "" );
+
+            // Empty strings, null, undefined and "auto" are converted to 0.
+            return !result || result === "auto" ? 0 : result;
+        },
+        set: function( tween ) {
+
+            // Use step hook for back compat.
+            // Use cssHook if its there.
+            // Use .style if available and use plain properties where available.
+            if ( jQuery.fx.step[ tween.prop ] ) {
+                jQuery.fx.step[ tween.prop ]( tween );
+            } else if ( tween.elem.nodeType === 1 &&
+                ( tween.elem.style[ jQuery.cssProps[ tween.prop ] ] != null ||
+                    jQuery.cssHooks[ tween.prop ] ) ) {
+                jQuery.style( tween.elem, tween.prop, tween.now + tween.unit );
+            } else {
+                tween.elem[ tween.prop ] = tween.now;
+            }
+        }
+    }
+};
+
+// Support: IE9
+// Panic based approach to setting things on disconnected nodes
+Tween.propHooks.scrollTop = Tween.propHooks.scrollLeft = {
+    set: function( tween ) {
+        if ( tween.elem.nodeType && tween.elem.parentNode ) {
+            tween.elem[ tween.prop ] = tween.now;
+        }
+    }
+};
+
+jQuery.easing = {
+    linear: function( p ) {
+        return p;
+    },
+    swing: function( p ) {
+        return 0.5 - Math.cos( p * Math.PI ) / 2;
+    },
+    _default: "swing"
+};
+
+jQuery.fx = Tween.prototype.init;
+
+// Back Compat <1.8 extension point
+jQuery.fx.step = {};
+
+
+
+
+var
+    fxNow, timerId,
+    rfxtypes = /^(?:toggle|show|hide)$/,
+    rrun = /queueHooks$/;
+
+// Animations created synchronously will run synchronously
+function createFxNow() {
+    window.setTimeout( function() {
+        fxNow = undefined;
+    } );
+    return ( fxNow = jQuery.now() );
+}
+
+// Generate parameters to create a standard animation
+function genFx( type, includeWidth ) {
+    var which,
+        i = 0,
+        attrs = { height: type };
+
+    // If we include width, step value is 1 to do all cssExpand values,
+    // otherwise step value is 2 to skip over Left and Right
+    includeWidth = includeWidth ? 1 : 0;
+    for ( ; i < 4 ; i += 2 - includeWidth ) {
+        which = cssExpand[ i ];
+        attrs[ "margin" + which ] = attrs[ "padding" + which ] = type;
+    }
+
+    if ( includeWidth ) {
+        attrs.opacity = attrs.width = type;
+    }
+
+    return attrs;
+}
+
+function createTween( value, prop, animation ) {
+    var tween,
+        collection = ( Animation.tweeners[ prop ] || [] ).concat( Animation.tweeners[ "*" ] ),
+        index = 0,
+        length = collection.length;
+    for ( ; index < length; index++ ) {
+        if ( ( tween = collection[ index ].call( animation, prop, value ) ) ) {
+
+            // We're done with this property
+            return tween;
+        }
+    }
+}
+
+function defaultPrefilter( elem, props, opts ) {
+    /* jshint validthis: true */
+    var prop, value, toggle, tween, hooks, oldfire, display, checkDisplay,
+        anim = this,
+        orig = {},
+        style = elem.style,
+        hidden = elem.nodeType && isHidden( elem ),
+        dataShow = dataPriv.get( elem, "fxshow" );
+
+    // Handle queue: false promises
+    if ( !opts.queue ) {
+        hooks = jQuery._queueHooks( elem, "fx" );
+        if ( hooks.unqueued == null ) {
+            hooks.unqueued = 0;
+            oldfire = hooks.empty.fire;
+            hooks.empty.fire = function() {
+                if ( !hooks.unqueued ) {
+                    oldfire();
+                }
+            };
+        }
+        hooks.unqueued++;
+
+        anim.always( function() {
+
+            // Ensure the complete handler is called before this completes
+            anim.always( function() {
+                hooks.unqueued--;
+                if ( !jQuery.queue( elem, "fx" ).length ) {
+                    hooks.empty.fire();
+                }
+            } );
+        } );
+    }
+
+    // Height/width overflow pass
+    if ( elem.nodeType === 1 && ( "height" in props || "width" in props ) ) {
+
+        // Make sure that nothing sneaks out
+        // Record all 3 overflow attributes because IE9-10 do not
+        // change the overflow attribute when overflowX and
+        // overflowY are set to the same value
+        opts.overflow = [ style.overflow, style.overflowX, style.overflowY ];
+
+        // Set display property to inline-block for height/width
+        // animations on inline elements that are having width/height animated
+        display = jQuery.css( elem, "display" );
+
+        // Test default display if display is currently "none"
+        checkDisplay = display === "none" ?
+            dataPriv.get( elem, "olddisplay" ) || defaultDisplay( elem.nodeName ) : display;
+
+        if ( checkDisplay === "inline" && jQuery.css( elem, "float" ) === "none" ) {
+            style.display = "inline-block";
+        }
+    }
+
+    if ( opts.overflow ) {
+        style.overflow = "hidden";
+        anim.always( function() {
+            style.overflow = opts.overflow[ 0 ];
+            style.overflowX = opts.overflow[ 1 ];
+            style.overflowY = opts.overflow[ 2 ];
+        } );
+    }
+
+    // show/hide pass
+    for ( prop in props ) {
+        value = props[ prop ];
+        if ( rfxtypes.exec( value ) ) {
+            delete props[ prop ];
+            toggle = toggle || value === "toggle";
+            if ( value === ( hidden ? "hide" : "show" ) ) {
+
+                // If there is dataShow left over from a stopped hide or show
+                // and we are going to proceed with show, we should pretend to be hidden
+                if ( value === "show" && dataShow && dataShow[ prop ] !== undefined ) {
+                    hidden = true;
+                } else {
+                    continue;
+                }
+            }
+            orig[ prop ] = dataShow && dataShow[ prop ] || jQuery.style( elem, prop );
+
+        // Any non-fx value stops us from restoring the original display value
+        } else {
+            display = undefined;
+        }
+    }
+
+    if ( !jQuery.isEmptyObject( orig ) ) {
+        if ( dataShow ) {
+            if ( "hidden" in dataShow ) {
+                hidden = dataShow.hidden;
+            }
+        } else {
+            dataShow = dataPriv.access( elem, "fxshow", {} );
+        }
+
+        // Store state if its toggle - enables .stop().toggle() to "reverse"
+        if ( toggle ) {
+            dataShow.hidden = !hidden;
+        }
+        if ( hidden ) {
+            jQuery( elem ).show();
+        } else {
+            anim.done( function() {
+                jQuery( elem ).hide();
+            } );
+        }
+        anim.done( function() {
+            var prop;
+
+            dataPriv.remove( elem, "fxshow" );
+            for ( prop in orig ) {
+                jQuery.style( elem, prop, orig[ prop ] );
+            }
+        } );
+        for ( prop in orig ) {
+            tween = createTween( hidden ? dataShow[ prop ] : 0, prop, anim );
+
+            if ( !( prop in dataShow ) ) {
+                dataShow[ prop ] = tween.start;
+                if ( hidden ) {
+                    tween.end = tween.start;
+                    tween.start = prop === "width" || prop === "height" ? 1 : 0;
+                }
+            }
+        }
+
+    // If this is a noop like .hide().hide(), restore an overwritten display value
+    } else if ( ( display === "none" ? defaultDisplay( elem.nodeName ) : display ) === "inline" ) {
+        style.display = display;
+    }
+}
+
+function propFilter( props, specialEasing ) {
+    var index, name, easing, value, hooks;
+
+    // camelCase, specialEasing and expand cssHook pass
+    for ( index in props ) {
+        name = jQuery.camelCase( index );
+        easing = specialEasing[ name ];
+        value = props[ index ];
+        if ( jQuery.isArray( value ) ) {
+            easing = value[ 1 ];
+            value = props[ index ] = value[ 0 ];
+        }
+
+        if ( index !== name ) {
+            props[ name ] = value;
+            delete props[ index ];
+        }
+
+        hooks = jQuery.cssHooks[ name ];
+        if ( hooks && "expand" in hooks ) {
+            value = hooks.expand( value );
+            delete props[ name ];
+
+            // Not quite $.extend, this won't overwrite existing keys.
+            // Reusing 'index' because we have the correct "name"
+            for ( index in value ) {
+                if ( !( index in props ) ) {
+                    props[ index ] = value[ index ];
+                    specialEasing[ index ] = easing;
+                }
+            }
+        } else {
+            specialEasing[ name ] = easing;
+        }
+    }
+}
+
+function Animation( elem, properties, options ) {
+    var result,
+        stopped,
+        index = 0,
+        length = Animation.prefilters.length,
+        deferred = jQuery.Deferred().always( function() {
+
+            // Don't match elem in the :animated selector
+            delete tick.elem;
+        } ),
+        tick = function() {
+            if ( stopped ) {
+                return false;
+            }
+            var currentTime = fxNow || createFxNow(),
+                remaining = Math.max( 0, animation.startTime + animation.duration - currentTime ),
+
+                // Support: Android 2.3
+                // Archaic crash bug won't allow us to use `1 - ( 0.5 || 0 )` (#12497)
+                temp = remaining / animation.duration || 0,
+                percent = 1 - temp,
+                index = 0,
+                length = animation.tweens.length;
+
+            for ( ; index < length ; index++ ) {
+                animation.tweens[ index ].run( percent );
+            }
+
+            deferred.notifyWith( elem, [ animation, percent, remaining ] );
+
+            if ( percent < 1 && length ) {
+                return remaining;
+            } else {
+                deferred.resolveWith( elem, [ animation ] );
+                return false;
+            }
+        },
+        animation = deferred.promise( {
+            elem: elem,
+            props: jQuery.extend( {}, properties ),
+            opts: jQuery.extend( true, {
+                specialEasing: {},
+                easing: jQuery.easing._default
+            }, options ),
+            originalProperties: properties,
+            originalOptions: options,
+            startTime: fxNow || createFxNow(),
+            duration: options.duration,
+            tweens: [],
+            createTween: function( prop, end ) {
+                var tween = jQuery.Tween( elem, animation.opts, prop, end,
+                        animation.opts.specialEasing[ prop ] || animation.opts.easing );
+                animation.tweens.push( tween );
+                return tween;
+            },
+            stop: function( gotoEnd ) {
+                var index = 0,
+
+                    // If we are going to the end, we want to run all the tweens
+                    // otherwise we skip this part
+                    length = gotoEnd ? animation.tweens.length : 0;
+                if ( stopped ) {
+                    return this;
+                }
+                stopped = true;
+                for ( ; index < length ; index++ ) {
+                    animation.tweens[ index ].run( 1 );
+                }
+
+                // Resolve when we played the last frame; otherwise, reject
+                if ( gotoEnd ) {
+                    deferred.notifyWith( elem, [ animation, 1, 0 ] );
+                    deferred.resolveWith( elem, [ animation, gotoEnd ] );
+                } else {
+                    deferred.rejectWith( elem, [ animation, gotoEnd ] );
+                }
+                return this;
+            }
+        } ),
+        props = animation.props;
+
+    propFilter( props, animation.opts.specialEasing );
+
+    for ( ; index < length ; index++ ) {
+        result = Animation.prefilters[ index ].call( animation, elem, props, animation.opts );
+        if ( result ) {
+            if ( jQuery.isFunction( result.stop ) ) {
+                jQuery._queueHooks( animation.elem, animation.opts.queue ).stop =
+                    jQuery.proxy( result.stop, result );
+            }
+            return result;
+        }
+    }
+
+    jQuery.map( props, createTween, animation );
+
+    if ( jQuery.isFunction( animation.opts.start ) ) {
+        animation.opts.start.call( elem, animation );
+    }
+
+    jQuery.fx.timer(
+        jQuery.extend( tick, {
+            elem: elem,
+            anim: animation,
+            queue: animation.opts.queue
+        } )
+    );
+
+    // attach callbacks from options
+    return animation.progress( animation.opts.progress )
+        .done( animation.opts.done, animation.opts.complete )
+        .fail( animation.opts.fail )
+        .always( animation.opts.always );
+}
+
+jQuery.Animation = jQuery.extend( Animation, {
+    tweeners: {
+        "*": [ function( prop, value ) {
+            var tween = this.createTween( prop, value );
+            adjustCSS( tween.elem, prop, rcssNum.exec( value ), tween );
+            return tween;
+        } ]
+    },
+
+    tweener: function( props, callback ) {
+        if ( jQuery.isFunction( props ) ) {
+            callback = props;
+            props = [ "*" ];
+        } else {
+            props = props.match( rnotwhite );
+        }
+
+        var prop,
+            index = 0,
+            length = props.length;
+
+        for ( ; index < length ; index++ ) {
+            prop = props[ index ];
+            Animation.tweeners[ prop ] = Animation.tweeners[ prop ] || [];
+            Animation.tweeners[ prop ].unshift( callback );
+        }
+    },
+
+    prefilters: [ defaultPrefilter ],
+
+    prefilter: function( callback, prepend ) {
+        if ( prepend ) {
+            Animation.prefilters.unshift( callback );
+        } else {
+            Animation.prefilters.push( callback );
+        }
+    }
+} );
+
+jQuery.speed = function( speed, easing, fn ) {
+    var opt = speed && typeof speed === "object" ? jQuery.extend( {}, speed ) : {
+        complete: fn || !fn && easing ||
+            jQuery.isFunction( speed ) && speed,
+        duration: speed,
+        easing: fn && easing || easing && !jQuery.isFunction( easing ) && easing
+    };
+
+    opt.duration = jQuery.fx.off ? 0 : typeof opt.duration === "number" ?
+        opt.duration : opt.duration in jQuery.fx.speeds ?
+            jQuery.fx.speeds[ opt.duration ] : jQuery.fx.speeds._default;
+
+    // Normalize opt.queue - true/undefined/null -> "fx"
+    if ( opt.queue == null || opt.queue === true ) {
+        opt.queue = "fx";
+    }
+
+    // Queueing
+    opt.old = opt.complete;
+
+    opt.complete = function() {
+        if ( jQuery.isFunction( opt.old ) ) {
+            opt.old.call( this );
+        }
+
+        if ( opt.queue ) {
+            jQuery.dequeue( this, opt.queue );
+        }
+    };
+
+    return opt;
+};
+
+jQuery.fn.extend( {
+    fadeTo: function( speed, to, easing, callback ) {
+
+        // Show any hidden elements after setting opacity to 0
+        return this.filter( isHidden ).css( "opacity", 0 ).show()
+
+            // Animate to the value specified
+            .end().animate( { opacity: to }, speed, easing, callback );
+    },
+    animate: function( prop, speed, easing, callback ) {
+        var empty = jQuery.isEmptyObject( prop ),
+            optall = jQuery.speed( speed, easing, callback ),
+            doAnimation = function() {
+
+                // Operate on a copy of prop so per-property easing won't be lost
+                var anim = Animation( this, jQuery.extend( {}, prop ), optall );
+
+                // Empty animations, or finishing resolves immediately
+                if ( empty || dataPriv.get( this, "finish" ) ) {
+                    anim.stop( true );
+                }
+            };
+            doAnimation.finish = doAnimation;
+
+        return empty || optall.queue === false ?
+            this.each( doAnimation ) :
+            this.queue( optall.queue, doAnimation );
+    },
+    stop: function( type, clearQueue, gotoEnd ) {
+        var stopQueue = function( hooks ) {
+            var stop = hooks.stop;
+            delete hooks.stop;
+            stop( gotoEnd );
+        };
+
+        if ( typeof type !== "string" ) {
+            gotoEnd = clearQueue;
+            clearQueue = type;
+            type = undefined;
+        }
+        if ( clearQueue && type !== false ) {
+            this.queue( type || "fx", [] );
+        }
+
+        return this.each( function() {
+            var dequeue = true,
+                index = type != null && type + "queueHooks",
+                timers = jQuery.timers,
+                data = dataPriv.get( this );
+
+            if ( index ) {
+                if ( data[ index ] && data[ index ].stop ) {
+                    stopQueue( data[ index ] );
+                }
+            } else {
+                for ( index in data ) {
+                    if ( data[ index ] && data[ index ].stop && rrun.test( index ) ) {
+                        stopQueue( data[ index ] );
+                    }
+                }
+            }
+
+            for ( index = timers.length; index--; ) {
+                if ( timers[ index ].elem === this &&
+                    ( type == null || timers[ index ].queue === type ) ) {
+
+                    timers[ index ].anim.stop( gotoEnd );
+                    dequeue = false;
+                    timers.splice( index, 1 );
+                }
+            }
+
+            // Start the next in the queue if the last step wasn't forced.
+            // Timers currently will call their complete callbacks, which
+            // will dequeue but only if they were gotoEnd.
+            if ( dequeue || !gotoEnd ) {
+                jQuery.dequeue( this, type );
+            }
+        } );
+    },
+    finish: function( type ) {
+        if ( type !== false ) {
+            type = type || "fx";
+        }
+        return this.each( function() {
+            var index,
+                data = dataPriv.get( this ),
+                queue = data[ type + "queue" ],
+                hooks = data[ type + "queueHooks" ],
+                timers = jQuery.timers,
+                length = queue ? queue.length : 0;
+
+            // Enable finishing flag on private data
+            data.finish = true;
+
+            // Empty the queue first
+            jQuery.queue( this, type, [] );
+
+            if ( hooks && hooks.stop ) {
+                hooks.stop.call( this, true );
+            }
+
+            // Look for any active animations, and finish them
+            for ( index = timers.length; index--; ) {
+                if ( timers[ index ].elem === this && timers[ index ].queue === type ) {
+                    timers[ index ].anim.stop( true );
+                    timers.splice( index, 1 );
+                }
+            }
+
+            // Look for any animations in the old queue and finish them
+            for ( index = 0; index < length; index++ ) {
+                if ( queue[ index ] && queue[ index ].finish ) {
+                    queue[ index ].finish.call( this );
+                }
+            }
+
+            // Turn off finishing flag
+            delete data.finish;
+        } );
+    }
+} );
+
+jQuery.each( [ "toggle", "show", "hide" ], function( i, name ) {
+    var cssFn = jQuery.fn[ name ];
+    jQuery.fn[ name ] = function( speed, easing, callback ) {
+        return speed == null || typeof speed === "boolean" ?
+            cssFn.apply( this, arguments ) :
+            this.animate( genFx( name, true ), speed, easing, callback );
+    };
+} );
+
+// Generate shortcuts for custom animations
+jQuery.each( {
+    slideDown: genFx( "show" ),
+    slideUp: genFx( "hide" ),
+    slideToggle: genFx( "toggle" ),
+    fadeIn: { opacity: "show" },
+    fadeOut: { opacity: "hide" },
+    fadeToggle: { opacity: "toggle" }
+}, function( name, props ) {
+    jQuery.fn[ name ] = function( speed, easing, callback ) {
+        return this.animate( props, speed, easing, callback );
+    };
+} );
+
+jQuery.timers = [];
+jQuery.fx.tick = function() {
+    var timer,
+        i = 0,
+        timers = jQuery.timers;
+
+    fxNow = jQuery.now();
+
+    for ( ; i < timers.length; i++ ) {
+        timer = timers[ i ];
+
+        // Checks the timer has not already been removed
+        if ( !timer() && timers[ i ] === timer ) {
+            timers.splice( i--, 1 );
+        }
+    }
+
+    if ( !timers.length ) {
+        jQuery.fx.stop();
+    }
+    fxNow = undefined;
+};
+
+jQuery.fx.timer = function( timer ) {
+    jQuery.timers.push( timer );
+    if ( timer() ) {
+        jQuery.fx.start();
+    } else {
+        jQuery.timers.pop();
+    }
+};
+
+jQuery.fx.interval = 13;
+jQuery.fx.start = function() {
+    if ( !timerId ) {
+        timerId = window.setInterval( jQuery.fx.tick, jQuery.fx.interval );
+    }
+};
+
+jQuery.fx.stop = function() {
+    window.clearInterval( timerId );
+
+    timerId = null;
+};
+
+jQuery.fx.speeds = {
+    slow: 600,
+    fast: 200,
+
+    // Default speed
+    _default: 400
+};
+
+
+// Based off of the plugin by Clint Helfers, with permission.
+// http://web.archive.org/web/20100324014747/http://blindsignals.com/index.php/2009/07/jquery-delay/
+jQuery.fn.delay = function( time, type ) {
+    time = jQuery.fx ? jQuery.fx.speeds[ time ] || time : time;
+    type = type || "fx";
+
+    return this.queue( type, function( next, hooks ) {
+        var timeout = window.setTimeout( next, time );
+        hooks.stop = function() {
+            window.clearTimeout( timeout );
+        };
+    } );
+};
+
+
+( function() {
+    var input = document.createElement( "input" ),
+        select = document.createElement( "select" ),
+        opt = select.appendChild( document.createElement( "option" ) );
+
+    input.type = "checkbox";
+
+    // Support: iOS<=5.1, Android<=4.2+
+    // Default value for a checkbox should be "on"
+    support.checkOn = input.value !== "";
+
+    // Support: IE<=11+
+    // Must access selectedIndex to make default options select
+    support.optSelected = opt.selected;
+
+    // Support: Android<=2.3
+    // Options inside disabled selects are incorrectly marked as disabled
+    select.disabled = true;
+    support.optDisabled = !opt.disabled;
+
+    // Support: IE<=11+
+    // An input loses its value after becoming a radio
+    input = document.createElement( "input" );
+    input.value = "t";
+    input.type = "radio";
+    support.radioValue = input.value === "t";
+} )();
+
+
+var boolHook,
+    attrHandle = jQuery.expr.attrHandle;
+
+jQuery.fn.extend( {
+    attr: function( name, value ) {
+        return access( this, jQuery.attr, name, value, arguments.length > 1 );
+    },
+
+    removeAttr: function( name ) {
+        return this.each( function() {
+            jQuery.removeAttr( this, name );
+        } );
+    }
+} );
+
+jQuery.extend( {
+    attr: function( elem, name, value ) {
+        var ret, hooks,
+            nType = elem.nodeType;
+
+        // Don't get/set attributes on text, comment and attribute nodes
+        if ( nType === 3 || nType === 8 || nType === 2 ) {
+            return;
+        }
+
+        // Fallback to prop when attributes are not supported
+        if ( typeof elem.getAttribute === "undefined" ) {
+            return jQuery.prop( elem, name, value );
+        }
+
+        // All attributes are lowercase
+        // Grab necessary hook if one is defined
+        if ( nType !== 1 || !jQuery.isXMLDoc( elem ) ) {
+            name = name.toLowerCase();
+            hooks = jQuery.attrHooks[ name ] ||
+                ( jQuery.expr.match.bool.test( name ) ? boolHook : undefined );
+        }
+
+        if ( value !== undefined ) {
+            if ( value === null ) {
+                jQuery.removeAttr( elem, name );
+                return;
+            }
+
+            if ( hooks && "set" in hooks &&
+                ( ret = hooks.set( elem, value, name ) ) !== undefined ) {
+                return ret;
+            }
+
+            elem.setAttribute( name, value + "" );
+            return value;
+        }
+
+        if ( hooks && "get" in hooks && ( ret = hooks.get( elem, name ) ) !== null ) {
+            return ret;
+        }
+
+        ret = jQuery.find.attr( elem, name );
+
+        // Non-existent attributes return null, we normalize to undefined
+        return ret == null ? undefined : ret;
+    },
+
+    attrHooks: {
+        type: {
+            set: function( elem, value ) {
+                if ( !support.radioValue && value === "radio" &&
+                    jQuery.nodeName( elem, "input" ) ) {
+                    var val = elem.value;
+                    elem.setAttribute( "type", value );
+                    if ( val ) {
+                        elem.value = val;
+                    }
+                    return value;
+                }
+            }
+        }
+    },
+
+    removeAttr: function( elem, value ) {
+        var name, propName,
+            i = 0,
+            attrNames = value && value.match( rnotwhite );
+
+        if ( attrNames && elem.nodeType === 1 ) {
+            while ( ( name = attrNames[ i++ ] ) ) {
+                propName = jQuery.propFix[ name ] || name;
+
+                // Boolean attributes get special treatment (#10870)
+                if ( jQuery.expr.match.bool.test( name ) ) {
+
+                    // Set corresponding property to false
+                    elem[ propName ] = false;
+                }
+
+                elem.removeAttribute( name );
+            }
+        }
+    }
+} );
+
+// Hooks for boolean attributes
+boolHook = {
+    set: function( elem, value, name ) {
+        if ( value === false ) {
+
+            // Remove boolean attributes when set to false
+            jQuery.removeAttr( elem, name );
+        } else {
+            elem.setAttribute( name, name );
+        }
+        return name;
+    }
+};
+jQuery.each( jQuery.expr.match.bool.source.match( /\w+/g ), function( i, name ) {
+    var getter = attrHandle[ name ] || jQuery.find.attr;
+
+    attrHandle[ name ] = function( elem, name, isXML ) {
+        var ret, handle;
+        if ( !isXML ) {
+
+            // Avoid an infinite loop by temporarily removing this function from the getter
+            handle = attrHandle[ name ];
+            attrHandle[ name ] = ret;
+            ret = getter( elem, name, isXML ) != null ?
+                name.toLowerCase() :
+                null;
+            attrHandle[ name ] = handle;
+        }
+        return ret;
+    };
+} );
+
+
+
+
+var rfocusable = /^(?:input|select|textarea|button)$/i,
+    rclickable = /^(?:a|area)$/i;
+
+jQuery.fn.extend( {
+    prop: function( name, value ) {
+        return access( this, jQuery.prop, name, value, arguments.length > 1 );
+    },
+
+    removeProp: function( name ) {
+        return this.each( function() {
+            delete this[ jQuery.propFix[ name ] || name ];
+        } );
+    }
+} );
+
+jQuery.extend( {
+    prop: function( elem, name, value ) {
+        var ret, hooks,
+            nType = elem.nodeType;
+
+        // Don't get/set properties on text, comment and attribute nodes
+        if ( nType === 3 || nType === 8 || nType === 2 ) {
+            return;
+        }
+
+        if ( nType !== 1 || !jQuery.isXMLDoc( elem ) ) {
+
+            // Fix name and attach hooks
+            name = jQuery.propFix[ name ] || name;
+            hooks = jQuery.propHooks[ name ];
+        }
+
+        if ( value !== undefined ) {
+            if ( hooks && "set" in hooks &&
+                ( ret = hooks.set( elem, value, name ) ) !== undefined ) {
+                return ret;
+            }
+
+            return ( elem[ name ] = value );
+        }
+
+        if ( hooks && "get" in hooks && ( ret = hooks.get( elem, name ) ) !== null ) {
+            return ret;
+        }
+
+        return elem[ name ];
+    },
+
+    propHooks: {
+        tabIndex: {
+            get: function( elem ) {
+
+                // elem.tabIndex doesn't always return the
+                // correct value when it hasn't been explicitly set
+                // http://fluidproject.org/blog/2008/01/09/getting-setting-and-removing-tabindex-values-with-javascript/
+                // Use proper attribute retrieval(#12072)
+                var tabindex = jQuery.find.attr( elem, "tabindex" );
+
+                return tabindex ?
+                    parseInt( tabindex, 10 ) :
+                    rfocusable.test( elem.nodeName ) ||
+                        rclickable.test( elem.nodeName ) && elem.href ?
+                            0 :
+                            -1;
+            }
+        }
+    },
+
+    propFix: {
+        "for": "htmlFor",
+        "class": "className"
+    }
+} );
+
+// Support: IE <=11 only
+// Accessing the selectedIndex property
+// forces the browser to respect setting selected
+// on the option
+// The getter ensures a default option is selected
+// when in an optgroup
+if ( !support.optSelected ) {
+    jQuery.propHooks.selected = {
+        get: function( elem ) {
+            var parent = elem.parentNode;
+            if ( parent && parent.parentNode ) {
+                parent.parentNode.selectedIndex;
+            }
+            return null;
+        },
+        set: function( elem ) {
+            var parent = elem.parentNode;
+            if ( parent ) {
+                parent.selectedIndex;
+
+                if ( parent.parentNode ) {
+                    parent.parentNode.selectedIndex;
+                }
+            }
+        }
+    };
+}
+
+jQuery.each( [
+    "tabIndex",
+    "readOnly",
+    "maxLength",
+    "cellSpacing",
+    "cellPadding",
+    "rowSpan",
+    "colSpan",
+    "useMap",
+    "frameBorder",
+    "contentEditable"
+], function() {
+    jQuery.propFix[ this.toLowerCase() ] = this;
+} );
+
+
+
+
+var rclass = /[\t\r\n\f]/g;
+
+function getClass( elem ) {
+    return elem.getAttribute && elem.getAttribute( "class" ) || "";
+}
+
+jQuery.fn.extend( {
+    addClass: function( value ) {
+        var classes, elem, cur, curValue, clazz, j, finalValue,
+            i = 0;
+
+        if ( jQuery.isFunction( value ) ) {
+            return this.each( function( j ) {
+                jQuery( this ).addClass( value.call( this, j, getClass( this ) ) );
+            } );
+        }
+
+        if ( typeof value === "string" && value ) {
+            classes = value.match( rnotwhite ) || [];
+
+            while ( ( elem = this[ i++ ] ) ) {
+                curValue = getClass( elem );
+                cur = elem.nodeType === 1 &&
+                    ( " " + curValue + " " ).replace( rclass, " " );
+
+                if ( cur ) {
+                    j = 0;
+                    while ( ( clazz = classes[ j++ ] ) ) {
+                        if ( cur.indexOf( " " + clazz + " " ) < 0 ) {
+                            cur += clazz + " ";
+                        }
+                    }
+
+                    // Only assign if different to avoid unneeded rendering.
+                    finalValue = jQuery.trim( cur );
+                    if ( curValue !== finalValue ) {
+                        elem.setAttribute( "class", finalValue );
+                    }
+                }
+            }
+        }
+
+        return this;
+    },
+
+    removeClass: function( value ) {
+        var classes, elem, cur, curValue, clazz, j, finalValue,
+            i = 0;
+
+        if ( jQuery.isFunction( value ) ) {
+            return this.each( function( j ) {
+                jQuery( this ).removeClass( value.call( this, j, getClass( this ) ) );
+            } );
+        }
+
+        if ( !arguments.length ) {
+            return this.attr( "class", "" );
+        }
+
+        if ( typeof value === "string" && value ) {
+            classes = value.match( rnotwhite ) || [];
+
+            while ( ( elem = this[ i++ ] ) ) {
+                curValue = getClass( elem );
+
+                // This expression is here for better compressibility (see addClass)
+                cur = elem.nodeType === 1 &&
+                    ( " " + curValue + " " ).replace( rclass, " " );
+
+                if ( cur ) {
+                    j = 0;
+                    while ( ( clazz = classes[ j++ ] ) ) {
+
+                        // Remove *all* instances
+                        while ( cur.indexOf( " " + clazz + " " ) > -1 ) {
+                            cur = cur.replace( " " + clazz + " ", " " );
+                        }
+                    }
+
+                    // Only assign if different to avoid unneeded rendering.
+                    finalValue = jQuery.trim( cur );
+                    if ( curValue !== finalValue ) {
+                        elem.setAttribute( "class", finalValue );
+                    }
+                }
+            }
+        }
+
+        return this;
+    },
+
+    toggleClass: function( value, stateVal ) {
+        var type = typeof value;
+
+        if ( typeof stateVal === "boolean" && type === "string" ) {
+            return stateVal ? this.addClass( value ) : this.removeClass( value );
+        }
+
+        if ( jQuery.isFunction( value ) ) {
+            return this.each( function( i ) {
+                jQuery( this ).toggleClass(
+                    value.call( this, i, getClass( this ), stateVal ),
+                    stateVal
+                );
+            } );
+        }
+
+        return this.each( function() {
+            var className, i, self, classNames;
+
+            if ( type === "string" ) {
+
+                // Toggle individual class names
+                i = 0;
+                self = jQuery( this );
+                classNames = value.match( rnotwhite ) || [];
+
+                while ( ( className = classNames[ i++ ] ) ) {
+
+                    // Check each className given, space separated list
+                    if ( self.hasClass( className ) ) {
+                        self.removeClass( className );
+                    } else {
+                        self.addClass( className );
+                    }
+                }
+
+            // Toggle whole class name
+            } else if ( value === undefined || type === "boolean" ) {
+                className = getClass( this );
+                if ( className ) {
+
+                    // Store className if set
+                    dataPriv.set( this, "__className__", className );
+                }
+
+                // If the element has a class name or if we're passed `false`,
+                // then remove the whole classname (if there was one, the above saved it).
+                // Otherwise bring back whatever was previously saved (if anything),
+                // falling back to the empty string if nothing was stored.
+                if ( this.setAttribute ) {
+                    this.setAttribute( "class",
+                        className || value === false ?
+                        "" :
+                        dataPriv.get( this, "__className__" ) || ""
+                    );
+                }
+            }
+        } );
+    },
+
+    hasClass: function( selector ) {
+        var className, elem,
+            i = 0;
+
+        className = " " + selector + " ";
+        while ( ( elem = this[ i++ ] ) ) {
+            if ( elem.nodeType === 1 &&
+                ( " " + getClass( elem ) + " " ).replace( rclass, " " )
+                    .indexOf( className ) > -1
+            ) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+} );
+
+
+
+
+var rreturn = /\r/g,
+    rspaces = /[\x20\t\r\n\f]+/g;
+
+jQuery.fn.extend( {
+    val: function( value ) {
+        var hooks, ret, isFunction,
+            elem = this[ 0 ];
+
+        if ( !arguments.length ) {
+            if ( elem ) {
+                hooks = jQuery.valHooks[ elem.type ] ||
+                    jQuery.valHooks[ elem.nodeName.toLowerCase() ];
+
+                if ( hooks &&
+                    "get" in hooks &&
+                    ( ret = hooks.get( elem, "value" ) ) !== undefined
+                ) {
+                    return ret;
+                }
+
+                ret = elem.value;
+
+                return typeof ret === "string" ?
+
+                    // Handle most common string cases
+                    ret.replace( rreturn, "" ) :
+
+                    // Handle cases where value is null/undef or number
+                    ret == null ? "" : ret;
+            }
+
+            return;
+        }
+
+        isFunction = jQuery.isFunction( value );
+
+        return this.each( function( i ) {
+            var val;
+
+            if ( this.nodeType !== 1 ) {
+                return;
+            }
+
+            if ( isFunction ) {
+                val = value.call( this, i, jQuery( this ).val() );
+            } else {
+                val = value;
+            }
+
+            // Treat null/undefined as ""; convert numbers to string
+            if ( val == null ) {
+                val = "";
+
+            } else if ( typeof val === "number" ) {
+                val += "";
+
+            } else if ( jQuery.isArray( val ) ) {
+                val = jQuery.map( val, function( value ) {
+                    return value == null ? "" : value + "";
+                } );
+            }
+
+            hooks = jQuery.valHooks[ this.type ] || jQuery.valHooks[ this.nodeName.toLowerCase() ];
+
+            // If set returns undefined, fall back to normal setting
+            if ( !hooks || !( "set" in hooks ) || hooks.set( this, val, "value" ) === undefined ) {
+                this.value = val;
+            }
+        } );
+    }
+} );
+
+jQuery.extend( {
+    valHooks: {
+        option: {
+            get: function( elem ) {
+
+                var val = jQuery.find.attr( elem, "value" );
+                return val != null ?
+                    val :
+
+                    // Support: IE10-11+
+                    // option.text throws exceptions (#14686, #14858)
+                    // Strip and collapse whitespace
+                    // https://html.spec.whatwg.org/#strip-and-collapse-whitespace
+                    jQuery.trim( jQuery.text( elem ) ).replace( rspaces, " " );
+            }
+        },
+        select: {
+            get: function( elem ) {
+                var value, option,
+                    options = elem.options,
+                    index = elem.selectedIndex,
+                    one = elem.type === "select-one" || index < 0,
+                    values = one ? null : [],
+                    max = one ? index + 1 : options.length,
+                    i = index < 0 ?
+                        max :
+                        one ? index : 0;
+
+                // Loop through all the selected options
+                for ( ; i < max; i++ ) {
+                    option = options[ i ];
+
+                    // IE8-9 doesn't update selected after form reset (#2551)
+                    if ( ( option.selected || i === index ) &&
+
+                            // Don't return options that are disabled or in a disabled optgroup
+                            ( support.optDisabled ?
+                                !option.disabled : option.getAttribute( "disabled" ) === null ) &&
+                            ( !option.parentNode.disabled ||
+                                !jQuery.nodeName( option.parentNode, "optgroup" ) ) ) {
+
+                        // Get the specific value for the option
+                        value = jQuery( option ).val();
+
+                        // We don't need an array for one selects
+                        if ( one ) {
+                            return value;
+                        }
+
+                        // Multi-Selects return an array
+                        values.push( value );
+                    }
+                }
+
+                return values;
+            },
+
+            set: function( elem, value ) {
+                var optionSet, option,
+                    options = elem.options,
+                    values = jQuery.makeArray( value ),
+                    i = options.length;
+
+                while ( i-- ) {
+                    option = options[ i ];
+                    if ( option.selected =
+                        jQuery.inArray( jQuery.valHooks.option.get( option ), values ) > -1
+                    ) {
+                        optionSet = true;
+                    }
+                }
+
+                // Force browsers to behave consistently when non-matching value is set
+                if ( !optionSet ) {
+                    elem.selectedIndex = -1;
+                }
+                return values;
+            }
+        }
+    }
+} );
+
+// Radios and checkboxes getter/setter
+jQuery.each( [ "radio", "checkbox" ], function() {
+    jQuery.valHooks[ this ] = {
+        set: function( elem, value ) {
+            if ( jQuery.isArray( value ) ) {
+                return ( elem.checked = jQuery.inArray( jQuery( elem ).val(), value ) > -1 );
+            }
+        }
+    };
+    if ( !support.checkOn ) {
+        jQuery.valHooks[ this ].get = function( elem ) {
+            return elem.getAttribute( "value" ) === null ? "on" : elem.value;
+        };
+    }
+} );
+
+
+
+
+// Return jQuery for attributes-only inclusion
+
+
+var rfocusMorph = /^(?:focusinfocus|focusoutblur)$/;
+
+jQuery.extend( jQuery.event, {
+
+    trigger: function( event, data, elem, onlyHandlers ) {
+
+        var i, cur, tmp, bubbleType, ontype, handle, special,
+            eventPath = [ elem || document ],
+            type = hasOwn.call( event, "type" ) ? event.type : event,
+            namespaces = hasOwn.call( event, "namespace" ) ? event.namespace.split( "." ) : [];
+
+        cur = tmp = elem = elem || document;
+
+        // Don't do events on text and comment nodes
+        if ( elem.nodeType === 3 || elem.nodeType === 8 ) {
+            return;
+        }
+
+        // focus/blur morphs to focusin/out; ensure we're not firing them right now
+        if ( rfocusMorph.test( type + jQuery.event.triggered ) ) {
+            return;
+        }
+
+        if ( type.indexOf( "." ) > -1 ) {
+
+            // Namespaced trigger; create a regexp to match event type in handle()
+            namespaces = type.split( "." );
+            type = namespaces.shift();
+            namespaces.sort();
+        }
+        ontype = type.indexOf( ":" ) < 0 && "on" + type;
+
+        // Caller can pass in a jQuery.Event object, Object, or just an event type string
+        event = event[ jQuery.expando ] ?
+            event :
+            new jQuery.Event( type, typeof event === "object" && event );
+
+        // Trigger bitmask: & 1 for native handlers; & 2 for jQuery (always true)
+        event.isTrigger = onlyHandlers ? 2 : 3;
+        event.namespace = namespaces.join( "." );
+        event.rnamespace = event.namespace ?
+            new RegExp( "(^|\\.)" + namespaces.join( "\\.(?:.*\\.|)" ) + "(\\.|$)" ) :
+            null;
+
+        // Clean up the event in case it is being reused
+        event.result = undefined;
+        if ( !event.target ) {
+            event.target = elem;
+        }
+
+        // Clone any incoming data and prepend the event, creating the handler arg list
+        data = data == null ?
+            [ event ] :
+            jQuery.makeArray( data, [ event ] );
+
+        // Allow special events to draw outside the lines
+        special = jQuery.event.special[ type ] || {};
+        if ( !onlyHandlers && special.trigger && special.trigger.apply( elem, data ) === false ) {
+            return;
+        }
+
+        // Determine event propagation path in advance, per W3C events spec (#9951)
+        // Bubble up to document, then to window; watch for a global ownerDocument var (#9724)
+        if ( !onlyHandlers && !special.noBubble && !jQuery.isWindow( elem ) ) {
+
+            bubbleType = special.delegateType || type;
+            if ( !rfocusMorph.test( bubbleType + type ) ) {
+                cur = cur.parentNode;
+            }
+            for ( ; cur; cur = cur.parentNode ) {
+                eventPath.push( cur );
+                tmp = cur;
+            }
+
+            // Only add window if we got to document (e.g., not plain obj or detached DOM)
+            if ( tmp === ( elem.ownerDocument || document ) ) {
+                eventPath.push( tmp.defaultView || tmp.parentWindow || window );
+            }
+        }
+
+        // Fire handlers on the event path
+        i = 0;
+        while ( ( cur = eventPath[ i++ ] ) && !event.isPropagationStopped() ) {
+
+            event.type = i > 1 ?
+                bubbleType :
+                special.bindType || type;
+
+            // jQuery handler
+            handle = ( dataPriv.get( cur, "events" ) || {} )[ event.type ] &&
+                dataPriv.get( cur, "handle" );
+            if ( handle ) {
+                handle.apply( cur, data );
+            }
+
+            // Native handler
+            handle = ontype && cur[ ontype ];
+            if ( handle && handle.apply && acceptData( cur ) ) {
+                event.result = handle.apply( cur, data );
+                if ( event.result === false ) {
+                    event.preventDefault();
+                }
+            }
+        }
+        event.type = type;
+
+        // If nobody prevented the default action, do it now
+        if ( !onlyHandlers && !event.isDefaultPrevented() ) {
+
+            if ( ( !special._default ||
+                special._default.apply( eventPath.pop(), data ) === false ) &&
+                acceptData( elem ) ) {
+
+                // Call a native DOM method on the target with the same name name as the event.
+                // Don't do default actions on window, that's where global variables be (#6170)
+                if ( ontype && jQuery.isFunction( elem[ type ] ) && !jQuery.isWindow( elem ) ) {
+
+                    // Don't re-trigger an onFOO event when we call its FOO() method
+                    tmp = elem[ ontype ];
+
+                    if ( tmp ) {
+                        elem[ ontype ] = null;
+                    }
+
+                    // Prevent re-triggering of the same event, since we already bubbled it above
+                    jQuery.event.triggered = type;
+                    elem[ type ]();
+                    jQuery.event.triggered = undefined;
+
+                    if ( tmp ) {
+                        elem[ ontype ] = tmp;
+                    }
+                }
+            }
+        }
+
+        return event.result;
+    },
+
+    // Piggyback on a donor event to simulate a different one
+    // Used only for `focus(in | out)` events
+    simulate: function( type, elem, event ) {
+        var e = jQuery.extend(
+            new jQuery.Event(),
+            event,
+            {
+                type: type,
+                isSimulated: true
+            }
+        );
+
+        jQuery.event.trigger( e, null, elem );
+    }
+
+} );
+
+jQuery.fn.extend( {
+
+    trigger: function( type, data ) {
+        return this.each( function() {
+            jQuery.event.trigger( type, data, this );
+        } );
+    },
+    triggerHandler: function( type, data ) {
+        var elem = this[ 0 ];
+        if ( elem ) {
+            return jQuery.event.trigger( type, data, elem, true );
+        }
+    }
+} );
+
+
+jQuery.each( ( "blur focus focusin focusout load resize scroll unload click dblclick " +
+    "mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave " +
+    "change select submit keydown keypress keyup error contextmenu" ).split( " " ),
+    function( i, name ) {
+
+    // Handle event binding
+    jQuery.fn[ name ] = function( data, fn ) {
+        return arguments.length > 0 ?
+            this.on( name, null, data, fn ) :
+            this.trigger( name );
+    };
+} );
+
+jQuery.fn.extend( {
+    hover: function( fnOver, fnOut ) {
+        return this.mouseenter( fnOver ).mouseleave( fnOut || fnOver );
+    }
+} );
+
+
+
+
+support.focusin = "onfocusin" in window;
+
+
+// Support: Firefox
+// Firefox doesn't have focus(in | out) events
+// Related ticket - https://bugzilla.mozilla.org/show_bug.cgi?id=687787
+//
+// Support: Chrome, Safari
+// focus(in | out) events fire after focus & blur events,
+// which is spec violation - http://www.w3.org/TR/DOM-Level-3-Events/#events-focusevent-event-order
+// Related ticket - https://code.google.com/p/chromium/issues/detail?id=449857
+if ( !support.focusin ) {
+    jQuery.each( { focus: "focusin", blur: "focusout" }, function( orig, fix ) {
+
+        // Attach a single capturing handler on the document while someone wants focusin/focusout
+        var handler = function( event ) {
+            jQuery.event.simulate( fix, event.target, jQuery.event.fix( event ) );
+        };
+
+        jQuery.event.special[ fix ] = {
+            setup: function() {
+                var doc = this.ownerDocument || this,
+                    attaches = dataPriv.access( doc, fix );
+
+                if ( !attaches ) {
+                    doc.addEventListener( orig, handler, true );
+                }
+                dataPriv.access( doc, fix, ( attaches || 0 ) + 1 );
+            },
+            teardown: function() {
+                var doc = this.ownerDocument || this,
+                    attaches = dataPriv.access( doc, fix ) - 1;
+
+                if ( !attaches ) {
+                    doc.removeEventListener( orig, handler, true );
+                    dataPriv.remove( doc, fix );
+
+                } else {
+                    dataPriv.access( doc, fix, attaches );
+                }
+            }
+        };
+    } );
+}
+var location = window.location;
+
+var nonce = jQuery.now();
+
+var rquery = ( /\?/ );
+
+
+
+// Support: Android 2.3
+// Workaround failure to string-cast null input
+jQuery.parseJSON = function( data ) {
+    return JSON.parse( data + "" );
+};
+
+
+// Cross-browser xml parsing
+jQuery.parseXML = function( data ) {
+    var xml;
+    if ( !data || typeof data !== "string" ) {
+        return null;
+    }
+
+    // Support: IE9
+    try {
+        xml = ( new window.DOMParser() ).parseFromString( data, "text/xml" );
+    } catch ( e ) {
+        xml = undefined;
+    }
+
+    if ( !xml || xml.getElementsByTagName( "parsererror" ).length ) {
+        jQuery.error( "Invalid XML: " + data );
+    }
+    return xml;
+};
+
+
+var
+    rhash = /#.*$/,
+    rts = /([?&])_=[^&]*/,
+    rheaders = /^(.*?):[ \t]*([^\r\n]*)$/mg,
+
+    // #7653, #8125, #8152: local protocol detection
+    rlocalProtocol = /^(?:about|app|app-storage|.+-extension|file|res|widget):$/,
+    rnoContent = /^(?:GET|HEAD)$/,
+    rprotocol = /^\/\//,
+
+    /* Prefilters
+     * 1) They are useful to introduce custom dataTypes (see ajax/jsonp.js for an example)
+     * 2) These are called:
+     *    - BEFORE asking for a transport
+     *    - AFTER param serialization (s.data is a string if s.processData is true)
+     * 3) key is the dataType
+     * 4) the catchall symbol "*" can be used
+     * 5) execution will start with transport dataType and THEN continue down to "*" if needed
+     */
+    prefilters = {},
+
+    /* Transports bindings
+     * 1) key is the dataType
+     * 2) the catchall symbol "*" can be used
+     * 3) selection will start with transport dataType and THEN go to "*" if needed
+     */
+    transports = {},
+
+    // Avoid comment-prolog char sequence (#10098); must appease lint and evade compression
+    allTypes = "*/".concat( "*" ),
+
+    // Anchor tag for parsing the document origin
+    originAnchor = document.createElement( "a" );
+    originAnchor.href = location.href;
+
+// Base "constructor" for jQuery.ajaxPrefilter and jQuery.ajaxTransport
+function addToPrefiltersOrTransports( structure ) {
+
+    // dataTypeExpression is optional and defaults to "*"
+    return function( dataTypeExpression, func ) {
+
+        if ( typeof dataTypeExpression !== "string" ) {
+            func = dataTypeExpression;
+            dataTypeExpression = "*";
+        }
+
+        var dataType,
+            i = 0,
+            dataTypes = dataTypeExpression.toLowerCase().match( rnotwhite ) || [];
+
+        if ( jQuery.isFunction( func ) ) {
+
+            // For each dataType in the dataTypeExpression
+            while ( ( dataType = dataTypes[ i++ ] ) ) {
+
+                // Prepend if requested
+                if ( dataType[ 0 ] === "+" ) {
+                    dataType = dataType.slice( 1 ) || "*";
+                    ( structure[ dataType ] = structure[ dataType ] || [] ).unshift( func );
+
+                // Otherwise append
+                } else {
+                    ( structure[ dataType ] = structure[ dataType ] || [] ).push( func );
+                }
+            }
+        }
+    };
+}
+
+// Base inspection function for prefilters and transports
+function inspectPrefiltersOrTransports( structure, options, originalOptions, jqXHR ) {
+
+    var inspected = {},
+        seekingTransport = ( structure === transports );
+
+    function inspect( dataType ) {
+        var selected;
+        inspected[ dataType ] = true;
+        jQuery.each( structure[ dataType ] || [], function( _, prefilterOrFactory ) {
+            var dataTypeOrTransport = prefilterOrFactory( options, originalOptions, jqXHR );
+            if ( typeof dataTypeOrTransport === "string" &&
+                !seekingTransport && !inspected[ dataTypeOrTransport ] ) {
+
+                options.dataTypes.unshift( dataTypeOrTransport );
+                inspect( dataTypeOrTransport );
+                return false;
+            } else if ( seekingTransport ) {
+                return !( selected = dataTypeOrTransport );
+            }
+        } );
+        return selected;
+    }
+
+    return inspect( options.dataTypes[ 0 ] ) || !inspected[ "*" ] && inspect( "*" );
+}
+
+// A special extend for ajax options
+// that takes "flat" options (not to be deep extended)
+// Fixes #9887
+function ajaxExtend( target, src ) {
+    var key, deep,
+        flatOptions = jQuery.ajaxSettings.flatOptions || {};
+
+    for ( key in src ) {
+        if ( src[ key ] !== undefined ) {
+            ( flatOptions[ key ] ? target : ( deep || ( deep = {} ) ) )[ key ] = src[ key ];
+        }
+    }
+    if ( deep ) {
+        jQuery.extend( true, target, deep );
+    }
+
+    return target;
+}
+
+/* Handles responses to an ajax request:
+ * - finds the right dataType (mediates between content-type and expected dataType)
+ * - returns the corresponding response
+ */
+function ajaxHandleResponses( s, jqXHR, responses ) {
+
+    var ct, type, finalDataType, firstDataType,
+        contents = s.contents,
+        dataTypes = s.dataTypes;
+
+    // Remove auto dataType and get content-type in the process
+    while ( dataTypes[ 0 ] === "*" ) {
+        dataTypes.shift();
+        if ( ct === undefined ) {
+            ct = s.mimeType || jqXHR.getResponseHeader( "Content-Type" );
+        }
+    }
+
+    // Check if we're dealing with a known content-type
+    if ( ct ) {
+        for ( type in contents ) {
+            if ( contents[ type ] && contents[ type ].test( ct ) ) {
+                dataTypes.unshift( type );
+                break;
+            }
+        }
+    }
+
+    // Check to see if we have a response for the expected dataType
+    if ( dataTypes[ 0 ] in responses ) {
+        finalDataType = dataTypes[ 0 ];
+    } else {
+
+        // Try convertible dataTypes
+        for ( type in responses ) {
+            if ( !dataTypes[ 0 ] || s.converters[ type + " " + dataTypes[ 0 ] ] ) {
+                finalDataType = type;
+                break;
+            }
+            if ( !firstDataType ) {
+                firstDataType = type;
+            }
+        }
+
+        // Or just use first one
+        finalDataType = finalDataType || firstDataType;
+    }
+
+    // If we found a dataType
+    // We add the dataType to the list if needed
+    // and return the corresponding response
+    if ( finalDataType ) {
+        if ( finalDataType !== dataTypes[ 0 ] ) {
+            dataTypes.unshift( finalDataType );
+        }
+        return responses[ finalDataType ];
+    }
+}
+
+/* Chain conversions given the request and the original response
+ * Also sets the responseXXX fields on the jqXHR instance
+ */
+function ajaxConvert( s, response, jqXHR, isSuccess ) {
+    var conv2, current, conv, tmp, prev,
+        converters = {},
+
+        // Work with a copy of dataTypes in case we need to modify it for conversion
+        dataTypes = s.dataTypes.slice();
+
+    // Create converters map with lowercased keys
+    if ( dataTypes[ 1 ] ) {
+        for ( conv in s.converters ) {
+            converters[ conv.toLowerCase() ] = s.converters[ conv ];
+        }
+    }
+
+    current = dataTypes.shift();
+
+    // Convert to each sequential dataType
+    while ( current ) {
+
+        if ( s.responseFields[ current ] ) {
+            jqXHR[ s.responseFields[ current ] ] = response;
+        }
+
+        // Apply the dataFilter if provided
+        if ( !prev && isSuccess && s.dataFilter ) {
+            response = s.dataFilter( response, s.dataType );
+        }
+
+        prev = current;
+        current = dataTypes.shift();
+
+        if ( current ) {
+
+        // There's only work to do if current dataType is non-auto
+            if ( current === "*" ) {
+
+                current = prev;
+
+            // Convert response if prev dataType is non-auto and differs from current
+            } else if ( prev !== "*" && prev !== current ) {
+
+                // Seek a direct converter
+                conv = converters[ prev + " " + current ] || converters[ "* " + current ];
+
+                // If none found, seek a pair
+                if ( !conv ) {
+                    for ( conv2 in converters ) {
+
+                        // If conv2 outputs current
+                        tmp = conv2.split( " " );
+                        if ( tmp[ 1 ] === current ) {
+
+                            // If prev can be converted to accepted input
+                            conv = converters[ prev + " " + tmp[ 0 ] ] ||
+                                converters[ "* " + tmp[ 0 ] ];
+                            if ( conv ) {
+
+                                // Condense equivalence converters
+                                if ( conv === true ) {
+                                    conv = converters[ conv2 ];
+
+                                // Otherwise, insert the intermediate dataType
+                                } else if ( converters[ conv2 ] !== true ) {
+                                    current = tmp[ 0 ];
+                                    dataTypes.unshift( tmp[ 1 ] );
+                                }
+                                break;
+                            }
+                        }
+                    }
+                }
+
+                // Apply converter (if not an equivalence)
+                if ( conv !== true ) {
+
+                    // Unless errors are allowed to bubble, catch and return them
+                    if ( conv && s.throws ) {
+                        response = conv( response );
+                    } else {
+                        try {
+                            response = conv( response );
+                        } catch ( e ) {
+                            return {
+                                state: "parsererror",
+                                error: conv ? e : "No conversion from " + prev + " to " + current
+                            };
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    return { state: "success", data: response };
+}
+
+jQuery.extend( {
+
+    // Counter for holding the number of active queries
+    active: 0,
+
+    // Last-Modified header cache for next request
+    lastModified: {},
+    etag: {},
+
+    ajaxSettings: {
+        url: location.href,
+        type: "GET",
+        isLocal: rlocalProtocol.test( location.protocol ),
+        global: true,
+        processData: true,
+        async: true,
+        contentType: "application/x-www-form-urlencoded; charset=UTF-8",
+        /*
+        timeout: 0,
+        data: null,
+        dataType: null,
+        username: null,
+        password: null,
+        cache: null,
+        throws: false,
+        traditional: false,
+        headers: {},
+        */
+
+        accepts: {
+            "*": allTypes,
+            text: "text/plain",
+            html: "text/html",
+            xml: "application/xml, text/xml",
+            json: "application/json, text/javascript"
+        },
+
+        contents: {
+            xml: /\bxml\b/,
+            html: /\bhtml/,
+            json: /\bjson\b/
+        },
+
+        responseFields: {
+            xml: "responseXML",
+            text: "responseText",
+            json: "responseJSON"
+        },
+
+        // Data converters
+        // Keys separate source (or catchall "*") and destination types with a single space
+        converters: {
+
+            // Convert anything to text
+            "* text": String,
+
+            // Text to html (true = no transformation)
+            "text html": true,
+
+            // Evaluate text as a json expression
+            "text json": jQuery.parseJSON,
+
+            // Parse text as xml
+            "text xml": jQuery.parseXML
+        },
+
+        // For options that shouldn't be deep extended:
+        // you can add your own custom options here if
+        // and when you create one that shouldn't be
+        // deep extended (see ajaxExtend)
+        flatOptions: {
+            url: true,
+            context: true
+        }
+    },
+
+    // Creates a full fledged settings object into target
+    // with both ajaxSettings and settings fields.
+    // If target is omitted, writes into ajaxSettings.
+    ajaxSetup: function( target, settings ) {
+        return settings ?
+
+            // Building a settings object
+            ajaxExtend( ajaxExtend( target, jQuery.ajaxSettings ), settings ) :
+
+            // Extending ajaxSettings
+            ajaxExtend( jQuery.ajaxSettings, target );
+    },
+
+    ajaxPrefilter: addToPrefiltersOrTransports( prefilters ),
+    ajaxTransport: addToPrefiltersOrTransports( transports ),
+
+    // Main method
+    ajax: function( url, options ) {
+
+        // If url is an object, simulate pre-1.5 signature
+        if ( typeof url === "object" ) {
+            options = url;
+            url = undefined;
+        }
+
+        // Force options to be an object
+        options = options || {};
+
+        var transport,
+
+            // URL without anti-cache param
+            cacheURL,
+
+            // Response headers
+            responseHeadersString,
+            responseHeaders,
+
+            // timeout handle
+            timeoutTimer,
+
+            // Url cleanup var
+            urlAnchor,
+
+            // To know if global events are to be dispatched
+            fireGlobals,
+
+            // Loop variable
+            i,
+
+            // Create the final options object
+            s = jQuery.ajaxSetup( {}, options ),
+
+            // Callbacks context
+            callbackContext = s.context || s,
+
+            // Context for global events is callbackContext if it is a DOM node or jQuery collection
+            globalEventContext = s.context &&
+                ( callbackContext.nodeType || callbackContext.jquery ) ?
+                    jQuery( callbackContext ) :
+                    jQuery.event,
+
+            // Deferreds
+            deferred = jQuery.Deferred(),
+            completeDeferred = jQuery.Callbacks( "once memory" ),
+
+            // Status-dependent callbacks
+            statusCode = s.statusCode || {},
+
+            // Headers (they are sent all at once)
+            requestHeaders = {},
+            requestHeadersNames = {},
+
+            // The jqXHR state
+            state = 0,
+
+            // Default abort message
+            strAbort = "canceled",
+
+            // Fake xhr
+            jqXHR = {
+                readyState: 0,
+
+                // Builds headers hashtable if needed
+                getResponseHeader: function( key ) {
+                    var match;
+                    if ( state === 2 ) {
+                        if ( !responseHeaders ) {
+                            responseHeaders = {};
+                            while ( ( match = rheaders.exec( responseHeadersString ) ) ) {
+                                responseHeaders[ match[ 1 ].toLowerCase() ] = match[ 2 ];
+                            }
+                        }
+                        match = responseHeaders[ key.toLowerCase() ];
+                    }
+                    return match == null ? null : match;
+                },
+
+                // Raw string
+                getAllResponseHeaders: function() {
+                    return state === 2 ? responseHeadersString : null;
+                },
+
+                // Caches the header
+                setRequestHeader: function( name, value ) {
+                    var lname = name.toLowerCase();
+                    if ( !state ) {
+                        name = requestHeadersNames[ lname ] = requestHeadersNames[ lname ] || name;
+                        requestHeaders[ name ] = value;
+                    }
+                    return this;
+                },
+
+                // Overrides response content-type header
+                overrideMimeType: function( type ) {
+                    if ( !state ) {
+                        s.mimeType = type;
+                    }
+                    return this;
+                },
+
+                // Status-dependent callbacks
+                statusCode: function( map ) {
+                    var code;
+                    if ( map ) {
+                        if ( state < 2 ) {
+                            for ( code in map ) {
+
+                                // Lazy-add the new callback in a way that preserves old ones
+                                statusCode[ code ] = [ statusCode[ code ], map[ code ] ];
+                            }
+                        } else {
+
+                            // Execute the appropriate callbacks
+                            jqXHR.always( map[ jqXHR.status ] );
+                        }
+                    }
+                    return this;
+                },
+
+                // Cancel the request
+                abort: function( statusText ) {
+                    var finalText = statusText || strAbort;
+                    if ( transport ) {
+                        transport.abort( finalText );
+                    }
+                    done( 0, finalText );
+                    return this;
+                }
+            };
+
+        // Attach deferreds
+        deferred.promise( jqXHR ).complete = completeDeferred.add;
+        jqXHR.success = jqXHR.done;
+        jqXHR.error = jqXHR.fail;
+
+        // Remove hash character (#7531: and string promotion)
+        // Add protocol if not provided (prefilters might expect it)
+        // Handle falsy url in the settings object (#10093: consistency with old signature)
+        // We also use the url parameter if available
+        s.url = ( ( url || s.url || location.href ) + "" ).replace( rhash, "" )
+            .replace( rprotocol, location.protocol + "//" );
+
+        // Alias method option to type as per ticket #12004
+        s.type = options.method || options.type || s.method || s.type;
+
+        // Extract dataTypes list
+        s.dataTypes = jQuery.trim( s.dataType || "*" ).toLowerCase().match( rnotwhite ) || [ "" ];
+
+        // A cross-domain request is in order when the origin doesn't match the current origin.
+        if ( s.crossDomain == null ) {
+            urlAnchor = document.createElement( "a" );
+
+            // Support: IE8-11+
+            // IE throws exception if url is malformed, e.g. http://example.com:80x/
+            try {
+                urlAnchor.href = s.url;
+
+                // Support: IE8-11+
+                // Anchor's host property isn't correctly set when s.url is relative
+                urlAnchor.href = urlAnchor.href;
+                s.crossDomain = originAnchor.protocol + "//" + originAnchor.host !==
+                    urlAnchor.protocol + "//" + urlAnchor.host;
+            } catch ( e ) {
+
+                // If there is an error parsing the URL, assume it is crossDomain,
+                // it can be rejected by the transport if it is invalid
+                s.crossDomain = true;
+            }
+        }
+
+        // Convert data if not already a string
+        if ( s.data && s.processData && typeof s.data !== "string" ) {
+            s.data = jQuery.param( s.data, s.traditional );
+        }
+
+        // Apply prefilters
+        inspectPrefiltersOrTransports( prefilters, s, options, jqXHR );
+
+        // If request was aborted inside a prefilter, stop there
+        if ( state === 2 ) {
+            return jqXHR;
+        }
+
+        // We can fire global events as of now if asked to
+        // Don't fire events if jQuery.event is undefined in an AMD-usage scenario (#15118)
+        fireGlobals = jQuery.event && s.global;
+
+        // Watch for a new set of requests
+        if ( fireGlobals && jQuery.active++ === 0 ) {
+            jQuery.event.trigger( "ajaxStart" );
+        }
+
+        // Uppercase the type
+        s.type = s.type.toUpperCase();
+
+        // Determine if request has content
+        s.hasContent = !rnoContent.test( s.type );
+
+        // Save the URL in case we're toying with the If-Modified-Since
+        // and/or If-None-Match header later on
+        cacheURL = s.url;
+
+        // More options handling for requests with no content
+        if ( !s.hasContent ) {
+
+            // If data is available, append data to url
+            if ( s.data ) {
+                cacheURL = ( s.url += ( rquery.test( cacheURL ) ? "&" : "?" ) + s.data );
+
+                // #9682: remove data so that it's not used in an eventual retry
+                delete s.data;
+            }
+
+            // Add anti-cache in url if needed
+            if ( s.cache === false ) {
+                s.url = rts.test( cacheURL ) ?
+
+                    // If there is already a '_' parameter, set its value
+                    cacheURL.replace( rts, "$1_=" + nonce++ ) :
+
+                    // Otherwise add one to the end
+                    cacheURL + ( rquery.test( cacheURL ) ? "&" : "?" ) + "_=" + nonce++;
+            }
+        }
+
+        // Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode.
+        if ( s.ifModified ) {
+            if ( jQuery.lastModified[ cacheURL ] ) {
+                jqXHR.setRequestHeader( "If-Modified-Since", jQuery.lastModified[ cacheURL ] );
+            }
+            if ( jQuery.etag[ cacheURL ] ) {
+                jqXHR.setRequestHeader( "If-None-Match", jQuery.etag[ cacheURL ] );
+            }
+        }
+
+        // Set the correct header, if data is being sent
+        if ( s.data && s.hasContent && s.contentType !== false || options.contentType ) {
+            jqXHR.setRequestHeader( "Content-Type", s.contentType );
+        }
+
+        // Set the Accepts header for the server, depending on the dataType
+        jqXHR.setRequestHeader(
+            "Accept",
+            s.dataTypes[ 0 ] && s.accepts[ s.dataTypes[ 0 ] ] ?
+                s.accepts[ s.dataTypes[ 0 ] ] +
+                    ( s.dataTypes[ 0 ] !== "*" ? ", " + allTypes + "; q=0.01" : "" ) :
+                s.accepts[ "*" ]
+        );
+
+        // Check for headers option
+        for ( i in s.headers ) {
+            jqXHR.setRequestHeader( i, s.headers[ i ] );
+        }
+
+        // Allow custom headers/mimetypes and early abort
+        if ( s.beforeSend &&
+            ( s.beforeSend.call( callbackContext, jqXHR, s ) === false || state === 2 ) ) {
+
+            // Abort if not done already and return
+            return jqXHR.abort();
+        }
+
+        // Aborting is no longer a cancellation
+        strAbort = "abort";
+
+        // Install callbacks on deferreds
+        for ( i in { success: 1, error: 1, complete: 1 } ) {
+            jqXHR[ i ]( s[ i ] );
+        }
+
+        // Get transport
+        transport = inspectPrefiltersOrTransports( transports, s, options, jqXHR );
+
+        // If no transport, we auto-abort
+        if ( !transport ) {
+            done( -1, "No Transport" );
+        } else {
+            jqXHR.readyState = 1;
+
+            // Send global event
+            if ( fireGlobals ) {
+                globalEventContext.trigger( "ajaxSend", [ jqXHR, s ] );
+            }
+
+            // If request was aborted inside ajaxSend, stop there
+            if ( state === 2 ) {
+                return jqXHR;
+            }
+
+            // Timeout
+            if ( s.async && s.timeout > 0 ) {
+                timeoutTimer = window.setTimeout( function() {
+                    jqXHR.abort( "timeout" );
+                }, s.timeout );
+            }
+
+            try {
+                state = 1;
+                transport.send( requestHeaders, done );
+            } catch ( e ) {
+
+                // Propagate exception as error if not done
+                if ( state < 2 ) {
+                    done( -1, e );
+
+                // Simply rethrow otherwise
+                } else {
+                    throw e;
+                }
+            }
+        }
+
+        // Callback for when everything is done
+        function done( status, nativeStatusText, responses, headers ) {
+            var isSuccess, success, error, response, modified,
+                statusText = nativeStatusText;
+
+            // Called once
+            if ( state === 2 ) {
+                return;
+            }
+
+            // State is "done" now
+            state = 2;
+
+            // Clear timeout if it exists
+            if ( timeoutTimer ) {
+                window.clearTimeout( timeoutTimer );
+            }
+
+            // Dereference transport for early garbage collection
+            // (no matter how long the jqXHR object will be used)
+            transport = undefined;
+
+            // Cache response headers
+            responseHeadersString = headers || "";
+
+            // Set readyState
+            jqXHR.readyState = status > 0 ? 4 : 0;
+
+            // Determine if successful
+            isSuccess = status >= 200 && status < 300 || status === 304;
+
+            // Get response data
+            if ( responses ) {
+                response = ajaxHandleResponses( s, jqXHR, responses );
+            }
+
+            // Convert no matter what (that way responseXXX fields are always set)
+            response = ajaxConvert( s, response, jqXHR, isSuccess );
+
+            // If successful, handle type chaining
+            if ( isSuccess ) {
+
+                // Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode.
+                if ( s.ifModified ) {
+                    modified = jqXHR.getResponseHeader( "Last-Modified" );
+                    if ( modified ) {
+                        jQuery.lastModified[ cacheURL ] = modified;
+                    }
+                    modified = jqXHR.getResponseHeader( "etag" );
+                    if ( modified ) {
+                        jQuery.etag[ cacheURL ] = modified;
+                    }
+                }
+
+                // if no content
+                if ( status === 204 || s.type === "HEAD" ) {
+                    statusText = "nocontent";
+
+                // if not modified
+                } else if ( status === 304 ) {
+                    statusText = "notmodified";
+
+                // If we have data, let's convert it
+                } else {
+                    statusText = response.state;
+                    success = response.data;
+                    error = response.error;
+                    isSuccess = !error;
+                }
+            } else {
+
+                // Extract error from statusText and normalize for non-aborts
+                error = statusText;
+                if ( status || !statusText ) {
+                    statusText = "error";
+                    if ( status < 0 ) {
+                        status = 0;
+                    }
+                }
+            }
+
+            // Set data for the fake xhr object
+            jqXHR.status = status;
+            jqXHR.statusText = ( nativeStatusText || statusText ) + "";
+
+            // Success/Error
+            if ( isSuccess ) {
+                deferred.resolveWith( callbackContext, [ success, statusText, jqXHR ] );
+            } else {
+                deferred.rejectWith( callbackContext, [ jqXHR, statusText, error ] );
+            }
+
+            // Status-dependent callbacks
+            jqXHR.statusCode( statusCode );
+            statusCode = undefined;
+
+            if ( fireGlobals ) {
+                globalEventContext.trigger( isSuccess ? "ajaxSuccess" : "ajaxError",
+                    [ jqXHR, s, isSuccess ? success : error ] );
+            }
+
+            // Complete
+            completeDeferred.fireWith( callbackContext, [ jqXHR, statusText ] );
+
+            if ( fireGlobals ) {
+                globalEventContext.trigger( "ajaxComplete", [ jqXHR, s ] );
+
+                // Handle the global AJAX counter
+                if ( !( --jQuery.active ) ) {
+                    jQuery.event.trigger( "ajaxStop" );
+                }
+            }
+        }
+
+        return jqXHR;
+    },
+
+    getJSON: function( url, data, callback ) {
+        return jQuery.get( url, data, callback, "json" );
+    },
+
+    getScript: function( url, callback ) {
+        return jQuery.get( url, undefined, callback, "script" );
+    }
+} );
+
+jQuery.each( [ "get", "post" ], function( i, method ) {
+    jQuery[ method ] = function( url, data, callback, type ) {
+
+        // Shift arguments if data argument was omitted
+        if ( jQuery.isFunction( data ) ) {
+            type = type || callback;
+            callback = data;
+            data = undefined;
+        }
+
+        // The url can be an options object (which then must have .url)
+        return jQuery.ajax( jQuery.extend( {
+            url: url,
+            type: method,
+            dataType: type,
+            data: data,
+            success: callback
+        }, jQuery.isPlainObject( url ) && url ) );
+    };
+} );
+
+
+jQuery._evalUrl = function( url ) {
+    return jQuery.ajax( {
+        url: url,
+
+        // Make this explicit, since user can override this through ajaxSetup (#11264)
+        type: "GET",
+        dataType: "script",
+        async: false,
+        global: false,
+        "throws": true
+    } );
+};
+
+
+jQuery.fn.extend( {
+    wrapAll: function( html ) {
+        var wrap;
+
+        if ( jQuery.isFunction( html ) ) {
+            return this.each( function( i ) {
+                jQuery( this ).wrapAll( html.call( this, i ) );
+            } );
+        }
+
+        if ( this[ 0 ] ) {
+
+            // The elements to wrap the target around
+            wrap = jQuery( html, this[ 0 ].ownerDocument ).eq( 0 ).clone( true );
+
+            if ( this[ 0 ].parentNode ) {
+                wrap.insertBefore( this[ 0 ] );
+            }
+
+            wrap.map( function() {
+                var elem = this;
+
+                while ( elem.firstElementChild ) {
+                    elem = elem.firstElementChild;
+                }
+
+                return elem;
+            } ).append( this );
+        }
+
+        return this;
+    },
+
+    wrapInner: function( html ) {
+        if ( jQuery.isFunction( html ) ) {
+            return this.each( function( i ) {
+                jQuery( this ).wrapInner( html.call( this, i ) );
+            } );
+        }
+
+        return this.each( function() {
+            var self = jQuery( this ),
+                contents = self.contents();
+
+            if ( contents.length ) {
+                contents.wrapAll( html );
+
+            } else {
+                self.append( html );
+            }
+        } );
+    },
+
+    wrap: function( html ) {
+        var isFunction = jQuery.isFunction( html );
+
+        return this.each( function( i ) {
+            jQuery( this ).wrapAll( isFunction ? html.call( this, i ) : html );
+        } );
+    },
+
+    unwrap: function() {
+        return this.parent().each( function() {
+            if ( !jQuery.nodeName( this, "body" ) ) {
+                jQuery( this ).replaceWith( this.childNodes );
+            }
+        } ).end();
+    }
+} );
+
+
+jQuery.expr.filters.hidden = function( elem ) {
+    return !jQuery.expr.filters.visible( elem );
+};
+jQuery.expr.filters.visible = function( elem ) {
+
+    // Support: Opera <= 12.12
+    // Opera reports offsetWidths and offsetHeights less than zero on some elements
+    // Use OR instead of AND as the element is not visible if either is true
+    // See tickets #10406 and #13132
+    return elem.offsetWidth > 0 || elem.offsetHeight > 0 || elem.getClientRects().length > 0;
+};
+
+
+
+
+var r20 = /%20/g,
+    rbracket = /\[\]$/,
+    rCRLF = /\r?\n/g,
+    rsubmitterTypes = /^(?:submit|button|image|reset|file)$/i,
+    rsubmittable = /^(?:input|select|textarea|keygen)/i;
+
+function buildParams( prefix, obj, traditional, add ) {
+    var name;
+
+    if ( jQuery.isArray( obj ) ) {
+
+        // Serialize array item.
+        jQuery.each( obj, function( i, v ) {
+            if ( traditional || rbracket.test( prefix ) ) {
+
+                // Treat each array item as a scalar.
+                add( prefix, v );
+
+            } else {
+
+                // Item is non-scalar (array or object), encode its numeric index.
+                buildParams(
+                    prefix + "[" + ( typeof v === "object" && v != null ? i : "" ) + "]",
+                    v,
+                    traditional,
+                    add
+                );
+            }
+        } );
+
+    } else if ( !traditional && jQuery.type( obj ) === "object" ) {
+
+        // Serialize object item.
+        for ( name in obj ) {
+            buildParams( prefix + "[" + name + "]", obj[ name ], traditional, add );
+        }
+
+    } else {
+
+        // Serialize scalar item.
+        add( prefix, obj );
+    }
+}
+
+// Serialize an array of form elements or a set of
+// key/values into a query string
+jQuery.param = function( a, traditional ) {
+    var prefix,
+        s = [],
+        add = function( key, value ) {
+
+            // If value is a function, invoke it and return its value
+            value = jQuery.isFunction( value ) ? value() : ( value == null ? "" : value );
+            s[ s.length ] = encodeURIComponent( key ) + "=" + encodeURIComponent( value );
+        };
+
+    // Set traditional to true for jQuery <= 1.3.2 behavior.
+    if ( traditional === undefined ) {
+        traditional = jQuery.ajaxSettings && jQuery.ajaxSettings.traditional;
+    }
+
+    // If an array was passed in, assume that it is an array of form elements.
+    if ( jQuery.isArray( a ) || ( a.jquery && !jQuery.isPlainObject( a ) ) ) {
+
+        // Serialize the form elements
+        jQuery.each( a, function() {
+            add( this.name, this.value );
+        } );
+
+    } else {
+
+        // If traditional, encode the "old" way (the way 1.3.2 or older
+        // did it), otherwise encode params recursively.
+        for ( prefix in a ) {
+            buildParams( prefix, a[ prefix ], traditional, add );
+        }
+    }
+
+    // Return the resulting serialization
+    return s.join( "&" ).replace( r20, "+" );
+};
+
+jQuery.fn.extend( {
+    serialize: function() {
+        return jQuery.param( this.serializeArray() );
+    },
+    serializeArray: function() {
+        return this.map( function() {
+
+            // Can add propHook for "elements" to filter or add form elements
+            var elements = jQuery.prop( this, "elements" );
+            return elements ? jQuery.makeArray( elements ) : this;
+        } )
+        .filter( function() {
+            var type = this.type;
+
+            // Use .is( ":disabled" ) so that fieldset[disabled] works
+            return this.name && !jQuery( this ).is( ":disabled" ) &&
+                rsubmittable.test( this.nodeName ) && !rsubmitterTypes.test( type ) &&
+                ( this.checked || !rcheckableType.test( type ) );
+        } )
+        .map( function( i, elem ) {
+            var val = jQuery( this ).val();
+
+            return val == null ?
+                null :
+                jQuery.isArray( val ) ?
+                    jQuery.map( val, function( val ) {
+                        return { name: elem.name, value: val.replace( rCRLF, "\r\n" ) };
+                    } ) :
+                    { name: elem.name, value: val.replace( rCRLF, "\r\n" ) };
+        } ).get();
+    }
+} );
+
+
+jQuery.ajaxSettings.xhr = function() {
+    try {
+        return new window.XMLHttpRequest();
+    } catch ( e ) {}
+};
+
+var xhrSuccessStatus = {
+
+        // File protocol always yields status code 0, assume 200
+        0: 200,
+
+        // Support: IE9
+        // #1450: sometimes IE returns 1223 when it should be 204
+        1223: 204
+    },
+    xhrSupported = jQuery.ajaxSettings.xhr();
+
+support.cors = !!xhrSupported && ( "withCredentials" in xhrSupported );
+support.ajax = xhrSupported = !!xhrSupported;
+
+jQuery.ajaxTransport( function( options ) {
+    var callback, errorCallback;
+
+    // Cross domain only allowed if supported through XMLHttpRequest
+    if ( support.cors || xhrSupported && !options.crossDomain ) {
+        return {
+            send: function( headers, complete ) {
+                var i,
+                    xhr = options.xhr();
+
+                xhr.open(
+                    options.type,
+                    options.url,
+                    options.async,
+                    options.username,
+                    options.password
+                );
+
+                // Apply custom fields if provided
+                if ( options.xhrFields ) {
+                    for ( i in options.xhrFields ) {
+                        xhr[ i ] = options.xhrFields[ i ];
+                    }
+                }
+
+                // Override mime type if needed
+                if ( options.mimeType && xhr.overrideMimeType ) {
+                    xhr.overrideMimeType( options.mimeType );
+                }
+
+                // X-Requested-With header
+                // For cross-domain requests, seeing as conditions for a preflight are
+                // akin to a jigsaw puzzle, we simply never set it to be sure.
+                // (it can always be set on a per-request basis or even using ajaxSetup)
+                // For same-domain requests, won't change header if already provided.
+                if ( !options.crossDomain && !headers[ "X-Requested-With" ] ) {
+                    headers[ "X-Requested-With" ] = "XMLHttpRequest";
+                }
+
+                // Set headers
+                for ( i in headers ) {
+                    xhr.setRequestHeader( i, headers[ i ] );
+                }
+
+                // Callback
+                callback = function( type ) {
+                    return function() {
+                        if ( callback ) {
+                            callback = errorCallback = xhr.onload =
+                                xhr.onerror = xhr.onabort = xhr.onreadystatechange = null;
+
+                            if ( type === "abort" ) {
+                                xhr.abort();
+                            } else if ( type === "error" ) {
+
+                                // Support: IE9
+                                // On a manual native abort, IE9 throws
+                                // errors on any property access that is not readyState
+                                if ( typeof xhr.status !== "number" ) {
+                                    complete( 0, "error" );
+                                } else {
+                                    complete(
+
+                                        // File: protocol always yields status 0; see #8605, #14207
+                                        xhr.status,
+                                        xhr.statusText
+                                    );
+                                }
+                            } else {
+                                complete(
+                                    xhrSuccessStatus[ xhr.status ] || xhr.status,
+                                    xhr.statusText,
+
+                                    // Support: IE9 only
+                                    // IE9 has no XHR2 but throws on binary (trac-11426)
+                                    // For XHR2 non-text, let the caller handle it (gh-2498)
+                                    ( xhr.responseType || "text" ) !== "text"  ||
+                                    typeof xhr.responseText !== "string" ?
+                                        { binary: xhr.response } :
+                                        { text: xhr.responseText },
+                                    xhr.getAllResponseHeaders()
+                                );
+                            }
+                        }
+                    };
+                };
+
+                // Listen to events
+                xhr.onload = callback();
+                errorCallback = xhr.onerror = callback( "error" );
+
+                // Support: IE9
+                // Use onreadystatechange to replace onabort
+                // to handle uncaught aborts
+                if ( xhr.onabort !== undefined ) {
+                    xhr.onabort = errorCallback;
+                } else {
+                    xhr.onreadystatechange = function() {
+
+                        // Check readyState before timeout as it changes
+                        if ( xhr.readyState === 4 ) {
+
+                            // Allow onerror to be called first,
+                            // but that will not handle a native abort
+                            // Also, save errorCallback to a variable
+                            // as xhr.onerror cannot be accessed
+                            window.setTimeout( function() {
+                                if ( callback ) {
+                                    errorCallback();
+                                }
+                            } );
+                        }
+                    };
+                }
+
+                // Create the abort callback
+                callback = callback( "abort" );
+
+                try {
+
+                    // Do send the request (this may raise an exception)
+                    xhr.send( options.hasContent && options.data || null );
+                } catch ( e ) {
+
+                    // #14683: Only rethrow if this hasn't been notified as an error yet
+                    if ( callback ) {
+                        throw e;
+                    }
+                }
+            },
+
+            abort: function() {
+                if ( callback ) {
+                    callback();
+                }
+            }
+        };
+    }
+} );
+
+
+
+
+// Install script dataType
+jQuery.ajaxSetup( {
+    accepts: {
+        script: "text/javascript, application/javascript, " +
+            "application/ecmascript, application/x-ecmascript"
+    },
+    contents: {
+        script: /\b(?:java|ecma)script\b/
+    },
+    converters: {
+        "text script": function( text ) {
+            jQuery.globalEval( text );
+            return text;
+        }
+    }
+} );
+
+// Handle cache's special case and crossDomain
+jQuery.ajaxPrefilter( "script", function( s ) {
+    if ( s.cache === undefined ) {
+        s.cache = false;
+    }
+    if ( s.crossDomain ) {
+        s.type = "GET";
+    }
+} );
+
+// Bind script tag hack transport
+jQuery.ajaxTransport( "script", function( s ) {
+
+    // This transport only deals with cross domain requests
+    if ( s.crossDomain ) {
+        var script, callback;
+        return {
+            send: function( _, complete ) {
+                script = jQuery( "<script>" ).prop( {
+                    charset: s.scriptCharset,
+                    src: s.url
+                } ).on(
+                    "load error",
+                    callback = function( evt ) {
+                        script.remove();
+                        callback = null;
+                        if ( evt ) {
+                            complete( evt.type === "error" ? 404 : 200, evt.type );
+                        }
+                    }
+                );
+
+                // Use native DOM manipulation to avoid our domManip AJAX trickery
+                document.head.appendChild( script[ 0 ] );
+            },
+            abort: function() {
+                if ( callback ) {
+                    callback();
+                }
+            }
+        };
+    }
+} );
+
+
+
+
+var oldCallbacks = [],
+    rjsonp = /(=)\?(?=&|$)|\?\?/;
+
+// Default jsonp settings
+jQuery.ajaxSetup( {
+    jsonp: "callback",
+    jsonpCallback: function() {
+        var callback = oldCallbacks.pop() || ( jQuery.expando + "_" + ( nonce++ ) );
+        this[ callback ] = true;
+        return callback;
+    }
+} );
+
+// Detect, normalize options and install callbacks for jsonp requests
+jQuery.ajaxPrefilter( "json jsonp", function( s, originalSettings, jqXHR ) {
+
+    var callbackName, overwritten, responseContainer,
+        jsonProp = s.jsonp !== false && ( rjsonp.test( s.url ) ?
+            "url" :
+            typeof s.data === "string" &&
+                ( s.contentType || "" )
+                    .indexOf( "application/x-www-form-urlencoded" ) === 0 &&
+                rjsonp.test( s.data ) && "data"
+        );
+
+    // Handle iff the expected data type is "jsonp" or we have a parameter to set
+    if ( jsonProp || s.dataTypes[ 0 ] === "jsonp" ) {
+
+        // Get callback name, remembering preexisting value associated with it
+        callbackName = s.jsonpCallback = jQuery.isFunction( s.jsonpCallback ) ?
+            s.jsonpCallback() :
+            s.jsonpCallback;
+
+        // Insert callback into url or form data
+        if ( jsonProp ) {
+            s[ jsonProp ] = s[ jsonProp ].replace( rjsonp, "$1" + callbackName );
+        } else if ( s.jsonp !== false ) {
+            s.url += ( rquery.test( s.url ) ? "&" : "?" ) + s.jsonp + "=" + callbackName;
+        }
+
+        // Use data converter to retrieve json after script execution
+        s.converters[ "script json" ] = function() {
+            if ( !responseContainer ) {
+                jQuery.error( callbackName + " was not called" );
+            }
+            return responseContainer[ 0 ];
+        };
+
+        // Force json dataType
+        s.dataTypes[ 0 ] = "json";
+
+        // Install callback
+        overwritten = window[ callbackName ];
+        window[ callbackName ] = function() {
+            responseContainer = arguments;
+        };
+
+        // Clean-up function (fires after converters)
+        jqXHR.always( function() {
+
+            // If previous value didn't exist - remove it
+            if ( overwritten === undefined ) {
+                jQuery( window ).removeProp( callbackName );
+
+            // Otherwise restore preexisting value
+            } else {
+                window[ callbackName ] = overwritten;
+            }
+
+            // Save back as free
+            if ( s[ callbackName ] ) {
+
+                // Make sure that re-using the options doesn't screw things around
+                s.jsonpCallback = originalSettings.jsonpCallback;
+
+                // Save the callback name for future use
+                oldCallbacks.push( callbackName );
+            }
+
+            // Call if it was a function and we have a response
+            if ( responseContainer && jQuery.isFunction( overwritten ) ) {
+                overwritten( responseContainer[ 0 ] );
+            }
+
+            responseContainer = overwritten = undefined;
+        } );
+
+        // Delegate to script
+        return "script";
+    }
+} );
+
+
+
+
+// Argument "data" should be string of html
+// context (optional): If specified, the fragment will be created in this context,
+// defaults to document
+// keepScripts (optional): If true, will include scripts passed in the html string
+jQuery.parseHTML = function( data, context, keepScripts ) {
+    if ( !data || typeof data !== "string" ) {
+        return null;
+    }
+    if ( typeof context === "boolean" ) {
+        keepScripts = context;
+        context = false;
+    }
+    context = context || document;
+
+    var parsed = rsingleTag.exec( data ),
+        scripts = !keepScripts && [];
+
+    // Single tag
+    if ( parsed ) {
+        return [ context.createElement( parsed[ 1 ] ) ];
+    }
+
+    parsed = buildFragment( [ data ], context, scripts );
+
+    if ( scripts && scripts.length ) {
+        jQuery( scripts ).remove();
+    }
+
+    return jQuery.merge( [], parsed.childNodes );
+};
+
+
+// Keep a copy of the old load method
+var _load = jQuery.fn.load;
+
+/**
+ * Load a url into a page
+ */
+jQuery.fn.load = function( url, params, callback ) {
+    if ( typeof url !== "string" && _load ) {
+        return _load.apply( this, arguments );
+    }
+
+    var selector, type, response,
+        self = this,
+        off = url.indexOf( " " );
+
+    if ( off > -1 ) {
+        selector = jQuery.trim( url.slice( off ) );
+        url = url.slice( 0, off );
+    }
+
+    // If it's a function
+    if ( jQuery.isFunction( params ) ) {
+
+        // We assume that it's the callback
+        callback = params;
+        params = undefined;
+
+    // Otherwise, build a param string
+    } else if ( params && typeof params === "object" ) {
+        type = "POST";
+    }
+
+    // If we have elements to modify, make the request
+    if ( self.length > 0 ) {
+        jQuery.ajax( {
+            url: url,
+
+            // If "type" variable is undefined, then "GET" method will be used.
+            // Make value of this field explicit since
+            // user can override it through ajaxSetup method
+            type: type || "GET",
+            dataType: "html",
+            data: params
+        } ).done( function( responseText ) {
+
+            // Save response for use in complete callback
+            response = arguments;
+
+            self.html( selector ?
+
+                // If a selector was specified, locate the right elements in a dummy div
+                // Exclude scripts to avoid IE 'Permission Denied' errors
+                jQuery( "<div>" ).append( jQuery.parseHTML( responseText ) ).find( selector ) :
+
+                // Otherwise use the full result
+                responseText );
+
+        // If the request succeeds, this function gets "data", "status", "jqXHR"
+        // but they are ignored because response was set above.
+        // If it fails, this function gets "jqXHR", "status", "error"
+        } ).always( callback && function( jqXHR, status ) {
+            self.each( function() {
+                callback.apply( this, response || [ jqXHR.responseText, status, jqXHR ] );
+            } );
+        } );
+    }
+
+    return this;
+};
+
+
+
+
+// Attach a bunch of functions for handling common AJAX events
+jQuery.each( [
+    "ajaxStart",
+    "ajaxStop",
+    "ajaxComplete",
+    "ajaxError",
+    "ajaxSuccess",
+    "ajaxSend"
+], function( i, type ) {
+    jQuery.fn[ type ] = function( fn ) {
+        return this.on( type, fn );
+    };
+} );
+
+
+
+
+jQuery.expr.filters.animated = function( elem ) {
+    return jQuery.grep( jQuery.timers, function( fn ) {
+        return elem === fn.elem;
+    } ).length;
+};
+
+
+
+
+/**
+ * Gets a window from an element
+ */
+function getWindow( elem ) {
+    return jQuery.isWindow( elem ) ? elem : elem.nodeType === 9 && elem.defaultView;
+}
+
+jQuery.offset = {
+    setOffset: function( elem, options, i ) {
+        var curPosition, curLeft, curCSSTop, curTop, curOffset, curCSSLeft, calculatePosition,
+            position = jQuery.css( elem, "position" ),
+            curElem = jQuery( elem ),
+            props = {};
+
+        // Set position first, in-case top/left are set even on static elem
+        if ( position === "static" ) {
+            elem.style.position = "relative";
+        }
+
+        curOffset = curElem.offset();
+        curCSSTop = jQuery.css( elem, "top" );
+        curCSSLeft = jQuery.css( elem, "left" );
+        calculatePosition = ( position === "absolute" || position === "fixed" ) &&
+            ( curCSSTop + curCSSLeft ).indexOf( "auto" ) > -1;
+
+        // Need to be able to calculate position if either
+        // top or left is auto and position is either absolute or fixed
+        if ( calculatePosition ) {
+            curPosition = curElem.position();
+            curTop = curPosition.top;
+            curLeft = curPosition.left;
+
+        } else {
+            curTop = parseFloat( curCSSTop ) || 0;
+            curLeft = parseFloat( curCSSLeft ) || 0;
+        }
+
+        if ( jQuery.isFunction( options ) ) {
+
+            // Use jQuery.extend here to allow modification of coordinates argument (gh-1848)
+            options = options.call( elem, i, jQuery.extend( {}, curOffset ) );
+        }
+
+        if ( options.top != null ) {
+            props.top = ( options.top - curOffset.top ) + curTop;
+        }
+        if ( options.left != null ) {
+            props.left = ( options.left - curOffset.left ) + curLeft;
+        }
+
+        if ( "using" in options ) {
+            options.using.call( elem, props );
+
+        } else {
+            curElem.css( props );
+        }
+    }
+};
+
+jQuery.fn.extend( {
+    offset: function( options ) {
+        if ( arguments.length ) {
+            return options === undefined ?
+                this :
+                this.each( function( i ) {
+                    jQuery.offset.setOffset( this, options, i );
+                } );
+        }
+
+        var docElem, win,
+            elem = this[ 0 ],
+            box = { top: 0, left: 0 },
+            doc = elem && elem.ownerDocument;
+
+        if ( !doc ) {
+            return;
+        }
+
+        docElem = doc.documentElement;
+
+        // Make sure it's not a disconnected DOM node
+        if ( !jQuery.contains( docElem, elem ) ) {
+            return box;
+        }
+
+        box = elem.getBoundingClientRect();
+        win = getWindow( doc );
+        return {
+            top: box.top + win.pageYOffset - docElem.clientTop,
+            left: box.left + win.pageXOffset - docElem.clientLeft
+        };
+    },
+
+    position: function() {
+        if ( !this[ 0 ] ) {
+            return;
+        }
+
+        var offsetParent, offset,
+            elem = this[ 0 ],
+            parentOffset = { top: 0, left: 0 };
+
+        // Fixed elements are offset from window (parentOffset = {top:0, left: 0},
+        // because it is its only offset parent
+        if ( jQuery.css( elem, "position" ) === "fixed" ) {
+
+            // Assume getBoundingClientRect is there when computed position is fixed
+            offset = elem.getBoundingClientRect();
+
+        } else {
+
+            // Get *real* offsetParent
+            offsetParent = this.offsetParent();
+
+            // Get correct offsets
+            offset = this.offset();
+            if ( !jQuery.nodeName( offsetParent[ 0 ], "html" ) ) {
+                parentOffset = offsetParent.offset();
+            }
+
+            // Add offsetParent borders
+            parentOffset.top += jQuery.css( offsetParent[ 0 ], "borderTopWidth", true );
+            parentOffset.left += jQuery.css( offsetParent[ 0 ], "borderLeftWidth", true );
+        }
+
+        // Subtract parent offsets and element margins
+        return {
+            top: offset.top - parentOffset.top - jQuery.css( elem, "marginTop", true ),
+            left: offset.left - parentOffset.left - jQuery.css( elem, "marginLeft", true )
+        };
+    },
+
+    // This method will return documentElement in the following cases:
+    // 1) For the element inside the iframe without offsetParent, this method will return
+    //    documentElement of the parent window
+    // 2) For the hidden or detached element
+    // 3) For body or html element, i.e. in case of the html node - it will return itself
+    //
+    // but those exceptions were never presented as a real life use-cases
+    // and might be considered as more preferable results.
+    //
+    // This logic, however, is not guaranteed and can change at any point in the future
+    offsetParent: function() {
+        return this.map( function() {
+            var offsetParent = this.offsetParent;
+
+            while ( offsetParent && jQuery.css( offsetParent, "position" ) === "static" ) {
+                offsetParent = offsetParent.offsetParent;
+            }
+
+            return offsetParent || documentElement;
+        } );
+    }
+} );
+
+// Create scrollLeft and scrollTop methods
+jQuery.each( { scrollLeft: "pageXOffset", scrollTop: "pageYOffset" }, function( method, prop ) {
+    var top = "pageYOffset" === prop;
+
+    jQuery.fn[ method ] = function( val ) {
+        return access( this, function( elem, method, val ) {
+            var win = getWindow( elem );
+
+            if ( val === undefined ) {
+                return win ? win[ prop ] : elem[ method ];
+            }
+
+            if ( win ) {
+                win.scrollTo(
+                    !top ? val : win.pageXOffset,
+                    top ? val : win.pageYOffset
+                );
+
+            } else {
+                elem[ method ] = val;
+            }
+        }, method, val, arguments.length );
+    };
+} );
+
+// Support: Safari<7-8+, Chrome<37-44+
+// Add the top/left cssHooks using jQuery.fn.position
+// Webkit bug: https://bugs.webkit.org/show_bug.cgi?id=29084
+// Blink bug: https://code.google.com/p/chromium/issues/detail?id=229280
+// getComputedStyle returns percent when specified for top/left/bottom/right;
+// rather than make the css module depend on the offset module, just check for it here
+jQuery.each( [ "top", "left" ], function( i, prop ) {
+    jQuery.cssHooks[ prop ] = addGetHookIf( support.pixelPosition,
+        function( elem, computed ) {
+            if ( computed ) {
+                computed = curCSS( elem, prop );
+
+                // If curCSS returns percentage, fallback to offset
+                return rnumnonpx.test( computed ) ?
+                    jQuery( elem ).position()[ prop ] + "px" :
+                    computed;
+            }
+        }
+    );
+} );
+
+
+// Create innerHeight, innerWidth, height, width, outerHeight and outerWidth methods
+jQuery.each( { Height: "height", Width: "width" }, function( name, type ) {
+    jQuery.each( { padding: "inner" + name, content: type, "": "outer" + name },
+        function( defaultExtra, funcName ) {
+
+        // Margin is only for outerHeight, outerWidth
+        jQuery.fn[ funcName ] = function( margin, value ) {
+            var chainable = arguments.length && ( defaultExtra || typeof margin !== "boolean" ),
+                extra = defaultExtra || ( margin === true || value === true ? "margin" : "border" );
+
+            return access( this, function( elem, type, value ) {
+                var doc;
+
+                if ( jQuery.isWindow( elem ) ) {
+
+                    // As of 5/8/2012 this will yield incorrect results for Mobile Safari, but there
+                    // isn't a whole lot we can do. See pull request at this URL for discussion:
+                    // https://github.com/jquery/jquery/pull/764
+                    return elem.document.documentElement[ "client" + name ];
+                }
+
+                // Get document width or height
+                if ( elem.nodeType === 9 ) {
+                    doc = elem.documentElement;
+
+                    // Either scroll[Width/Height] or offset[Width/Height] or client[Width/Height],
+                    // whichever is greatest
+                    return Math.max(
+                        elem.body[ "scroll" + name ], doc[ "scroll" + name ],
+                        elem.body[ "offset" + name ], doc[ "offset" + name ],
+                        doc[ "client" + name ]
+                    );
+                }
+
+                return value === undefined ?
+
+                    // Get width or height on the element, requesting but not forcing parseFloat
+                    jQuery.css( elem, type, extra ) :
+
+                    // Set width or height on the element
+                    jQuery.style( elem, type, value, extra );
+            }, type, chainable ? margin : undefined, chainable, null );
+        };
+    } );
+} );
+
+
+jQuery.fn.extend( {
+
+    bind: function( types, data, fn ) {
+        return this.on( types, null, data, fn );
+    },
+    unbind: function( types, fn ) {
+        return this.off( types, null, fn );
+    },
+
+    delegate: function( selector, types, data, fn ) {
+        return this.on( types, selector, data, fn );
+    },
+    undelegate: function( selector, types, fn ) {
+
+        // ( namespace ) or ( selector, types [, fn] )
+        return arguments.length === 1 ?
+            this.off( selector, "**" ) :
+            this.off( types, selector || "**", fn );
+    },
+    size: function() {
+        return this.length;
+    }
+} );
+
+jQuery.fn.andSelf = jQuery.fn.addBack;
+
+
+
+
+// Register as a named AMD module, since jQuery can be concatenated with other
+// files that may use define, but not via a proper concatenation script that
+// understands anonymous AMD modules. A named AMD is safest and most robust
+// way to register. Lowercase jquery is used because AMD module names are
+// derived from file names, and jQuery is normally delivered in a lowercase
+// file name. Do this after creating the global so that if an AMD module wants
+// to call noConflict to hide this version of jQuery, it will work.
+
+// Note that for maximum portability, libraries that are not jQuery should
+// declare themselves as anonymous modules, and avoid setting a global if an
+// AMD loader is present. jQuery is a special case. For more information, see
+// https://github.com/jrburke/requirejs/wiki/Updating-existing-libraries#wiki-anon
+
+if ( typeof define === "function" && define.amd ) {
+    define( "jquery", [], function() {
+        return jQuery;
+    } );
+}
+
+
+
+var
+
+    // Map over jQuery in case of overwrite
+    _jQuery = window.jQuery,
+
+    // Map over the $ in case of overwrite
+    _$ = window.$;
+
+jQuery.noConflict = function( deep ) {
+    if ( window.$ === jQuery ) {
+        window.$ = _$;
+    }
+
+    if ( deep && window.jQuery === jQuery ) {
+        window.jQuery = _jQuery;
+    }
+
+    return jQuery;
+};
+
+// Expose jQuery and $ identifiers, even in AMD
+// (#7102#comment:10, https://github.com/jquery/jquery/pull/557)
+// and CommonJS for browser emulators (#13566)
+if ( !noGlobal ) {
+    window.jQuery = window.$ = jQuery;
+}
+
+return jQuery;
+}));

+ 3700 - 0
src/http/deps/jsv.js

@@ -0,0 +1,3700 @@
+// This is a build of https://github.com/garycourt/JSV at 0aa11852537069b0830569ef1eab11a36b65b3ab with jsv.js, schema03 and URI.js appended.
+// The build contains a few lines of custom code to handle format validation.
+// Custom code is wrapped in comments that start with "JOSHFIRE"
+(function(global, require) {
+var exports = {};
+
+
+
+/**
+ * URI.js
+ * 
+ * @fileoverview An RFC 3986 compliant, scheme extendable URI parsing/validating/resolving library for JavaScript.
+ * @author <a href="mailto:gary.court@gmail.com">Gary Court</a>
+ * @version 1.2
+ * @see http://github.com/garycourt/uri-js
+ * @license URI.js v1.2 (c) 2010 Gary Court. License: http://github.com/garycourt/uri-js
+ */
+
+/**
+ * Copyright 2010 Gary Court. 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 GARY COURT ``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 GARY COURT 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.
+ * 
+ * The views and conclusions contained in the software and documentation are those of the
+ * authors and should not be interpreted as representing official policies, either expressed
+ * or implied, of Gary Court.
+ */
+
+/*jslint white: true, sub: true, onevar: true, undef: true, eqeqeq: true, newcap: true, immed: true, indent: 4 */
+/*global exports:true, require:true */
+
+if (typeof exports === "undefined") {
+	exports = {}; 
+}
+if (typeof require !== "function") {
+	require = function (id) {
+		return exports;
+	};
+}
+(function () {
+	var	
+		/**
+		 * @param {...string} sets
+		 * @return {string}
+		 */
+		mergeSet = function (sets) {
+			var set = arguments[0],
+				x = 1,
+				nextSet = arguments[x];
+
+			while (nextSet) {
+				set = set.slice(0, -1) + nextSet.slice(1);
+				nextSet = arguments[++x];
+			}
+
+			return set;
+		},
+
+		/**
+		 * @param {string} str
+		 * @return {string}
+		 */
+		subexp = function (str) {
+			return "(?:" + str + ")";
+		},
+
+		ALPHA$$ = "[A-Za-z]",
+		CR$ = "[\\x0D]",
+		DIGIT$$ = "[0-9]",
+		DQUOTE$$ = "[\\x22]",
+		HEXDIG$$ = mergeSet(DIGIT$$, "[A-Fa-f]"),  //case-insensitive
+		LF$$ = "[\\x0A]",
+		SP$$ = "[\\x20]",
+		PCT_ENCODED$ = subexp("%" + HEXDIG$$ + HEXDIG$$),
+		GEN_DELIMS$$ = "[\\:\\/\\?\\#\\[\\]\\@]",
+		SUB_DELIMS$$ = "[\\!\\$\\&\\'\\(\\)\\*\\+\\,\\;\\=]",
+		RESERVED$$ = mergeSet(GEN_DELIMS$$, SUB_DELIMS$$),
+		UNRESERVED$$ = mergeSet(ALPHA$$, DIGIT$$, "[\\-\\.\\_\\~]"),
+		SCHEME$ = subexp(ALPHA$$ + mergeSet(ALPHA$$, DIGIT$$, "[\\+\\-\\.]") + "*"),
+		USERINFO$ = subexp(subexp(PCT_ENCODED$ + "|" + mergeSet(UNRESERVED$$, SUB_DELIMS$$, "[\\:]")) + "*"),
+		DEC_OCTET$ = subexp(subexp("25[0-5]") + "|" + subexp("2[0-4]" + DIGIT$$) + "|" + subexp("1" + DIGIT$$ + DIGIT$$) + "|" + subexp("[1-9]" + DIGIT$$) + "|" + DIGIT$$),
+		IPV4ADDRESS$ = subexp(DEC_OCTET$ + "\\." + DEC_OCTET$ + "\\." + DEC_OCTET$ + "\\." + DEC_OCTET$),
+		H16$ = subexp(HEXDIG$$ + "{1,4}"),
+		LS32$ = subexp(subexp(H16$ + "\\:" + H16$) + "|" + IPV4ADDRESS$),
+		IPV6ADDRESS$ = subexp(mergeSet(UNRESERVED$$, SUB_DELIMS$$, "[\\:]") + "+"),  //FIXME
+		IPVFUTURE$ = subexp("v" + HEXDIG$$ + "+\\." + mergeSet(UNRESERVED$$, SUB_DELIMS$$, "[\\:]") + "+"),
+		IP_LITERAL$ = subexp("\\[" + subexp(IPV6ADDRESS$ + "|" + IPVFUTURE$) + "\\]"),
+		REG_NAME$ = subexp(subexp(PCT_ENCODED$ + "|" + mergeSet(UNRESERVED$$, SUB_DELIMS$$)) + "*"),
+		HOST$ = subexp(IP_LITERAL$ + "|" + IPV4ADDRESS$ + "|" + REG_NAME$),
+		PORT$ = subexp(DIGIT$$ + "*"),
+		AUTHORITY$ = subexp(subexp(USERINFO$ + "@") + "?" + HOST$ + subexp("\\:" + PORT$) + "?"),
+		PCHAR$ = subexp(PCT_ENCODED$ + "|" + mergeSet(UNRESERVED$$, SUB_DELIMS$$, "[\\:\\@]")),
+		SEGMENT$ = subexp(PCHAR$ + "*"),
+		SEGMENT_NZ$ = subexp(PCHAR$ + "+"),
+		SEGMENT_NZ_NC$ = subexp(subexp(PCT_ENCODED$ + "|" + mergeSet(UNRESERVED$$, SUB_DELIMS$$, "[\\@]")) + "+"),
+		PATH_ABEMPTY$ = subexp(subexp("\\/" + SEGMENT$) + "*"),
+		PATH_ABSOLUTE$ = subexp("\\/" + subexp(SEGMENT_NZ$ + PATH_ABEMPTY$) + "?"),  //simplified
+		PATH_NOSCHEME$ = subexp(SEGMENT_NZ_NC$ + PATH_ABEMPTY$),  //simplified
+		PATH_ROOTLESS$ = subexp(SEGMENT_NZ$ + PATH_ABEMPTY$),  //simplified
+		PATH_EMPTY$ = subexp(""),  //simplified
+		PATH$ = subexp(PATH_ABEMPTY$ + "|" + PATH_ABSOLUTE$ + "|" + PATH_NOSCHEME$ + "|" + PATH_ROOTLESS$ + "|" + PATH_EMPTY$),
+		QUERY$ = subexp(subexp(PCHAR$ + "|[\\/\\?]") + "*"),
+		FRAGMENT$ = subexp(subexp(PCHAR$ + "|[\\/\\?]") + "*"),
+		HIER_PART$ = subexp(subexp("\\/\\/" + AUTHORITY$ + PATH_ABEMPTY$) + "|" + PATH_ABSOLUTE$ + "|" + PATH_ROOTLESS$ + "|" + PATH_EMPTY$),
+		URI$ = subexp(SCHEME$ + "\\:" + HIER_PART$ + subexp("\\?" + QUERY$) + "?" + subexp("\\#" + FRAGMENT$) + "?"),
+		RELATIVE_PART$ = subexp(subexp("\\/\\/" + AUTHORITY$ + PATH_ABEMPTY$) + "|" + PATH_ABSOLUTE$ + "|" + PATH_NOSCHEME$ + "|" + PATH_EMPTY$),
+		RELATIVE_REF$ = subexp(RELATIVE_PART$ + subexp("\\?" + QUERY$) + "?" + subexp("\\#" + FRAGMENT$) + "?"),
+		URI_REFERENCE$ = subexp(URI$ + "|" + RELATIVE_REF$),
+		ABSOLUTE_URI$ = subexp(SCHEME$ + "\\:" + HIER_PART$ + subexp("\\?" + QUERY$) + "?"),
+
+		URI_REF = new RegExp("^" + subexp("(" + URI$ + ")|(" + RELATIVE_REF$ + ")") + "$"),
+		GENERIC_REF  = new RegExp("^(" + SCHEME$ + ")\\:" + subexp(subexp("\\/\\/(" + subexp("(" + USERINFO$ + ")@") + "?(" + HOST$ + ")" + subexp("\\:(" + PORT$ + ")") + "?)") + "?(" + PATH_ABEMPTY$ + "|" + PATH_ABSOLUTE$ + "|" + PATH_ROOTLESS$ + "|" + PATH_EMPTY$ + ")") + subexp("\\?(" + QUERY$ + ")") + "?" + subexp("\\#(" + FRAGMENT$ + ")") + "?$"),
+		RELATIVE_REF = new RegExp("^(){0}" + subexp(subexp("\\/\\/(" + subexp("(" + USERINFO$ + ")@") + "?(" + HOST$ + ")" + subexp("\\:(" + PORT$ + ")") + "?)") + "?(" + PATH_ABEMPTY$ + "|" + PATH_ABSOLUTE$ + "|" + PATH_NOSCHEME$ + "|" + PATH_EMPTY$ + ")") + subexp("\\?(" + QUERY$ + ")") + "?" + subexp("\\#(" + FRAGMENT$ + ")") + "?$"),
+		ABSOLUTE_REF = new RegExp("^(" + SCHEME$ + ")\\:" + subexp(subexp("\\/\\/(" + subexp("(" + USERINFO$ + ")@") + "?(" + HOST$ + ")" + subexp("\\:(" + PORT$ + ")") + "?)") + "?(" + PATH_ABEMPTY$ + "|" + PATH_ABSOLUTE$ + "|" + PATH_ROOTLESS$ + "|" + PATH_EMPTY$ + ")") + subexp("\\?(" + QUERY$ + ")") + "?$"),
+		SAMEDOC_REF = new RegExp("^" + subexp("\\#(" + FRAGMENT$ + ")") + "?$"),
+		AUTHORITY = new RegExp("^" + subexp("(" + USERINFO$ + ")@") + "?(" + HOST$ + ")" + subexp("\\:(" + PORT$ + ")") + "?$"),
+
+		NOT_SCHEME = new RegExp(mergeSet("[^]", ALPHA$$, DIGIT$$, "[\\+\\-\\.]"), "g"),
+		NOT_USERINFO = new RegExp(mergeSet("[^\\%\\:]", UNRESERVED$$, SUB_DELIMS$$), "g"),
+		NOT_HOST = new RegExp(mergeSet("[^\\%]", UNRESERVED$$, SUB_DELIMS$$), "g"),
+		NOT_PATH = new RegExp(mergeSet("[^\\%\\/\\:\\@]", UNRESERVED$$, SUB_DELIMS$$), "g"),
+		NOT_PATH_NOSCHEME = new RegExp(mergeSet("[^\\%\\/\\@]", UNRESERVED$$, SUB_DELIMS$$), "g"),
+		NOT_QUERY = new RegExp(mergeSet("[^\\%]", UNRESERVED$$, SUB_DELIMS$$, "[\\:\\@\\/\\?]"), "g"),
+		NOT_FRAGMENT = NOT_QUERY,
+		ESCAPE = new RegExp(mergeSet("[^]", UNRESERVED$$, SUB_DELIMS$$), "g"),
+		UNRESERVED = new RegExp(UNRESERVED$$, "g"),
+		OTHER_CHARS = new RegExp(mergeSet("[^\\%]", UNRESERVED$$, RESERVED$$), "g"),
+		PCT_ENCODEDS = new RegExp(PCT_ENCODED$ + "+", "g"),
+		URI_PARSE = /^(?:([^:\/?#]+):)?(?:\/\/((?:([^\/?#@]*)@)?([^\/?#:]*)(?:\:(\d*))?))?([^?#]*)(?:\?([^#]*))?(?:#(.*))?/i,
+		RDS1 = /^\.\.?\//,
+		RDS2 = /^\/\.(\/|$)/,
+		RDS3 = /^\/\.\.(\/|$)/,
+		RDS4 = /^\.\.?$/,
+		RDS5 = /^\/?.*?(?=\/|$)/,
+		NO_MATCH_IS_UNDEFINED = ("").match(/(){0}/)[1] === undefined,
+
+		/**
+		 * @param {string} chr
+		 * @return {string}
+		 */
+		pctEncChar = function (chr) {
+			var c = chr.charCodeAt(0);
+ 
+			if (c < 128) {
+				return "%" + c.toString(16).toUpperCase();
+			}
+			else if ((c > 127) && (c < 2048)) {
+				return "%" + ((c >> 6) | 192).toString(16).toUpperCase() + "%" + ((c & 63) | 128).toString(16).toUpperCase();
+			}
+			else {
+				return "%" + ((c >> 12) | 224).toString(16).toUpperCase() + "%" + (((c >> 6) & 63) | 128).toString(16).toUpperCase() + "%" + ((c & 63) | 128).toString(16).toUpperCase();
+			}
+		},
+
+		/**
+		 * @param {string} str
+		 * @return {string}
+		 */
+		pctDecUnreserved = function (str) {
+			var newStr = "", 
+				i = 0,
+				c, s;
+
+			while (i < str.length) {
+				c = parseInt(str.substr(i + 1, 2), 16);
+
+				if (c < 128) {
+					s = String.fromCharCode(c);
+					if (s.match(UNRESERVED)) {
+						newStr += s;
+					} else {
+						newStr += str.substr(i, 3);
+					}
+					i += 3;
+				}
+				else if ((c > 191) && (c < 224)) {
+					newStr += str.substr(i, 6);
+					i += 6;
+				}
+				else {
+					newStr += str.substr(i, 9);
+					i += 9;
+				}
+			}
+
+			return newStr;
+		},
+
+		/**
+		 * @param {string} str
+		 * @return {string}
+		 */
+		pctDecChars = function (str) {
+			var newStr = "", 
+				i = 0,
+				c, c2, c3;
+
+			while (i < str.length) {
+				c = parseInt(str.substr(i + 1, 2), 16);
+
+				if (c < 128) {
+					newStr += String.fromCharCode(c);
+					i += 3;
+				}
+				else if ((c > 191) && (c < 224)) {
+					c2 = parseInt(str.substr(i + 4, 2), 16);
+					newStr += String.fromCharCode(((c & 31) << 6) | (c2 & 63));
+					i += 6;
+				}
+				else {
+					c2 = parseInt(str.substr(i + 4, 2), 16);
+					c3 = parseInt(str.substr(i + 7, 2), 16);
+					newStr += String.fromCharCode(((c & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63));
+					i += 9;
+				}
+			}
+
+			return newStr;
+		},
+
+		/**
+		 * @return {string}
+		 */
+		typeOf = function (o) {
+			return o === undefined ? "undefined" : (o === null ? "null" : Object.prototype.toString.call(o).split(" ").pop().split("]").shift().toLowerCase());
+		},
+
+		/**
+		 * @constructor
+		 * @implements URIComponents
+		 */
+		Components = function () {
+			this.errors = [];
+		}, 
+
+		/** @namespace */ 
+		URI = {};
+
+	/**
+	 * Components
+	 */
+
+	Components.prototype = {
+		/**
+		 * @type String
+		 */
+
+		scheme : undefined,
+
+		/**
+		 * @type String
+		 */
+
+		authority : undefined,
+
+		/**
+		 * @type String
+		 */
+
+		userinfo : undefined,
+
+		/**
+		 * @type String
+		 */
+
+		host : undefined,
+
+		/**
+		 * @type number
+		 */
+
+		port : undefined,
+
+		/**
+		 * @type string
+		 */
+
+		path : undefined,
+
+		/**
+		 * @type string
+		 */
+
+		query : undefined,
+
+		/**
+		 * @type string
+		 */
+
+		fragment : undefined,
+
+		/**
+		 * @type string
+		 * @values "uri", "absolute", "relative", "same-document"
+		 */
+
+		reference : undefined,
+
+		/**
+		 * @type Array
+		 */
+
+		errors : undefined
+	};
+
+	/**
+	 * URI
+	 */
+
+	/**
+	 * @namespace
+	 */
+
+	URI.SCHEMES = {};
+
+	/**
+	 * @param {string} uriString
+	 * @param {Options} [options]
+	 * @returns {URIComponents}
+	 */
+
+	URI.parse = function (uriString, options) {
+		var matches, 
+			components = new Components(),
+			schemeHandler;
+
+		uriString = uriString ? uriString.toString() : "";
+		options = options || {};
+
+		if (options.reference === "suffix") {
+			uriString = (options.scheme ? options.scheme + ":" : "") + "//" + uriString;
+		}
+
+		matches = uriString.match(URI_REF);
+
+		if (matches) {
+			if (matches[1]) {
+				//generic URI
+				matches = uriString.match(GENERIC_REF);
+			} else {
+				//relative URI
+				matches = uriString.match(RELATIVE_REF);
+			}
+		} 
+
+		if (!matches) {
+			if (!options.tolerant) {
+				components.errors.push("URI is not strictly valid.");
+			}
+			matches = uriString.match(URI_PARSE);
+		}
+
+		if (matches) {
+			if (NO_MATCH_IS_UNDEFINED) {
+				//store each component
+				components.scheme = matches[1];
+				components.authority = matches[2];
+				components.userinfo = matches[3];
+				components.host = matches[4];
+				components.port = parseInt(matches[5], 10);
+				components.path = matches[6] || "";
+				components.query = matches[7];
+				components.fragment = matches[8];
+
+				//fix port number
+				if (isNaN(components.port)) {
+					components.port = matches[5];
+				}
+			} else {  //IE FIX for improper RegExp matching
+				//store each component
+				components.scheme = matches[1] || undefined;
+				components.authority = (uriString.indexOf("//") !== -1 ? matches[2] : undefined);
+				components.userinfo = (uriString.indexOf("@") !== -1 ? matches[3] : undefined);
+				components.host = (uriString.indexOf("//") !== -1 ? matches[4] : undefined);
+				components.port = parseInt(matches[5], 10);
+				components.path = matches[6] || "";
+				components.query = (uriString.indexOf("?") !== -1 ? matches[7] : undefined);
+				components.fragment = (uriString.indexOf("#") !== -1 ? matches[8] : undefined);
+
+				//fix port number
+				if (isNaN(components.port)) {
+					components.port = (uriString.match(/\/\/.*\:(?:\/|\?|\#|$)/) ? matches[4] : undefined);
+				}
+			}
+
+			//determine reference type
+			if (!components.scheme && !components.authority && !components.path && !components.query) {
+				components.reference = "same-document";
+			} else if (!components.scheme) {
+				components.reference = "relative";
+			} else if (!components.fragment) {
+				components.reference = "absolute";
+			} else {
+				components.reference = "uri";
+			}
+
+			//check for reference errors
+			if (options.reference && options.reference !== "suffix" && options.reference !== components.reference) {
+				components.errors.push("URI is not a " + options.reference + " reference.");
+			}
+
+			//check if a handler for the scheme exists
+			schemeHandler = URI.SCHEMES[components.scheme || options.scheme];
+			if (schemeHandler && schemeHandler.parse) {
+				//perform extra parsing
+				schemeHandler.parse(components, options);
+			}
+		} else {
+			components.errors.push("URI can not be parsed.");
+		}
+
+		return components;
+	};
+
+	/**
+	 * @private
+	 * @param {URIComponents} components
+	 * @returns {string|undefined}
+	 */
+
+	URI._recomposeAuthority = function (components) {
+		var uriTokens = [];
+
+		if (components.userinfo !== undefined || components.host !== undefined || typeof components.port === "number") {
+			if (components.userinfo !== undefined) {
+				uriTokens.push(components.userinfo.toString().replace(NOT_USERINFO, pctEncChar));
+				uriTokens.push("@");
+			}
+			if (components.host !== undefined) {
+				uriTokens.push(components.host.toString().toLowerCase().replace(NOT_HOST, pctEncChar));
+			}
+			if (typeof components.port === "number") {
+				uriTokens.push(":");
+				uriTokens.push(components.port.toString(10));
+			}
+		}
+
+		return uriTokens.length ? uriTokens.join("") : undefined;
+	};
+
+	/**
+	 * @param {string} input
+	 * @returns {string}
+	 */
+
+	URI.removeDotSegments = function (input) {
+		var output = [], s;
+
+		while (input.length) {
+			if (input.match(RDS1)) {
+				input = input.replace(RDS1, "");
+			} else if (input.match(RDS2)) {
+				input = input.replace(RDS2, "/");
+			} else if (input.match(RDS3)) {
+				input = input.replace(RDS3, "/");
+				output.pop();
+			} else if (input === "." || input === "..") {
+				input = "";
+			} else {
+				s = input.match(RDS5)[0];
+				input = input.slice(s.length);
+				output.push(s);
+			}
+		}
+
+		return output.join("");
+	};
+
+	/**
+	 * @param {URIComponents} components
+	 * @param {Options} [options]
+	 * @returns {string}
+	 */
+
+	URI.serialize = function (components, options) {
+		var uriTokens = [], 
+			schemeHandler, 
+			s;
+		options = options || {};
+
+		//check if a handler for the scheme exists
+		schemeHandler = URI.SCHEMES[components.scheme || options.scheme];
+		if (schemeHandler && schemeHandler.serialize) {
+			//perform extra serialization
+			schemeHandler.serialize(components, options);
+		}
+
+		if (options.reference !== "suffix" && components.scheme) {
+			uriTokens.push(components.scheme.toString().toLowerCase().replace(NOT_SCHEME, ""));
+			uriTokens.push(":");
+		}
+
+		components.authority = URI._recomposeAuthority(components);
+		if (components.authority !== undefined) {
+			if (options.reference !== "suffix") {
+				uriTokens.push("//");
+			}
+
+			uriTokens.push(components.authority);
+
+			if (components.path && components.path.charAt(0) !== "/") {
+				uriTokens.push("/");
+			}
+		}
+
+		if (components.path) {
+			s = URI.removeDotSegments(components.path.toString().replace(/%2E/ig, "."));
+
+			if (components.scheme) {
+				s = s.replace(NOT_PATH, pctEncChar);
+			} else {
+				s = s.replace(NOT_PATH_NOSCHEME, pctEncChar);
+			}
+
+			if (components.authority === undefined) {
+				s = s.replace(/^\/\//, "/%2F");  //don't allow the path to start with "//"
+			}
+			uriTokens.push(s);
+		}
+
+		if (components.query) {
+			uriTokens.push("?");
+			uriTokens.push(components.query.toString().replace(NOT_QUERY, pctEncChar));
+		}
+
+		if (components.fragment) {
+			uriTokens.push("#");
+			uriTokens.push(components.fragment.toString().replace(NOT_FRAGMENT, pctEncChar));
+		}
+
+		return uriTokens
+			.join('')  //merge tokens into a string
+			.replace(PCT_ENCODEDS, pctDecUnreserved)  //undecode unreserved characters
+			//.replace(OTHER_CHARS, pctEncChar)  //replace non-URI characters
+			.replace(/%[0-9A-Fa-f]{2}/g, function (str) {  //uppercase percent encoded characters
+				return str.toUpperCase();
+			})
+		;
+	};
+
+	/**
+	 * @param {URIComponents} base
+	 * @param {URIComponents} relative
+	 * @param {Options} [options]
+	 * @param {boolean} [skipNormalization]
+	 * @returns {URIComponents}
+	 */
+
+	URI.resolveComponents = function (base, relative, options, skipNormalization) {
+		var target = new Components();
+
+		if (!skipNormalization) {
+			base = URI.parse(URI.serialize(base, options), options);  //normalize base components
+			relative = URI.parse(URI.serialize(relative, options), options);  //normalize relative components
+		}
+		options = options || {};
+
+		if (!options.tolerant && relative.scheme) {
+			target.scheme = relative.scheme;
+			target.authority = relative.authority;
+			target.userinfo = relative.userinfo;
+			target.host = relative.host;
+			target.port = relative.port;
+			target.path = URI.removeDotSegments(relative.path);
+			target.query = relative.query;
+		} else {
+			if (relative.authority !== undefined) {
+				target.authority = relative.authority;
+				target.userinfo = relative.userinfo;
+				target.host = relative.host;
+				target.port = relative.port;
+				target.path = URI.removeDotSegments(relative.path);
+				target.query = relative.query;
+			} else {
+				if (!relative.path) {
+					target.path = base.path;
+					if (relative.query !== undefined) {
+						target.query = relative.query;
+					} else {
+						target.query = base.query;
+					}
+				} else {
+					if (relative.path.charAt(0) === "/") {
+						target.path = URI.removeDotSegments(relative.path);
+					} else {
+						if (base.authority !== undefined && !base.path) {
+							target.path = "/" + relative.path;
+						} else if (!base.path) {
+							target.path = relative.path;
+						} else {
+							target.path = base.path.slice(0, base.path.lastIndexOf("/") + 1) + relative.path;
+						}
+						target.path = URI.removeDotSegments(target.path);
+					}
+					target.query = relative.query;
+				}
+				target.authority = base.authority;
+				target.userinfo = base.userinfo;
+				target.host = base.host;
+				target.port = base.port;
+			}
+			target.scheme = base.scheme;
+		}
+
+		target.fragment = relative.fragment;
+
+		return target;
+	};
+
+	/**
+	 * @param {string} baseURI
+	 * @param {string} relativeURI
+	 * @param {Options} [options]
+	 * @returns {string}
+	 */
+
+	URI.resolve = function (baseURI, relativeURI, options) {
+		return URI.serialize(URI.resolveComponents(URI.parse(baseURI, options), URI.parse(relativeURI, options), options, true), options);
+	};
+
+	/**
+	 * @param {string|URIComponents} uri
+	 * @param {Options} options
+	 * @returns {string|URIComponents}
+	 */
+
+	URI.normalize = function (uri, options) {
+		if (typeof uri === "string") {
+			return URI.serialize(URI.parse(uri, options), options);
+		} else if (typeOf(uri) === "object") {
+			return URI.parse(URI.serialize(uri, options), options);
+		}
+
+		return uri;
+	};
+
+	/**
+	 * @param {string|URIComponents} uriA
+	 * @param {string|URIComponents} uriB
+	 * @param {Options} options
+	 */
+
+	URI.equal = function (uriA, uriB, options) {
+		if (typeof uriA === "string") {
+			uriA = URI.serialize(URI.parse(uriA, options), options);
+		} else if (typeOf(uriA) === "object") {
+			uriA = URI.serialize(uriA, options);
+		}
+
+		if (typeof uriB === "string") {
+			uriB = URI.serialize(URI.parse(uriB, options), options);
+		} else if (typeOf(uriB) === "object") {
+			uriB = URI.serialize(uriB, options);
+		}
+
+		return uriA === uriB;
+	};
+
+	/**
+	 * @param {string} str
+	 * @returns {string}
+	 */
+
+	URI.escapeComponent = function (str) {
+		return str && str.toString().replace(ESCAPE, pctEncChar);
+	};
+
+	/**
+	 * @param {string} str
+	 * @returns {string}
+	 */
+
+	URI.unescapeComponent = function (str) {
+		return str && str.toString().replace(PCT_ENCODEDS, pctDecChars);
+	};
+
+	//export API
+	exports.Components = Components;
+	exports.URI = URI;
+
+	//name-safe export API
+	exports["URI"] = {
+		"SCHEMES" : URI.SCHEMES,
+		"parse" : URI.parse,
+		"removeDotSegments" : URI.removeDotSegments,
+		"serialize" : URI.serialize,
+		"resolveComponents" : URI.resolveComponents,
+		"resolve" : URI.resolve,
+		"normalize" : URI.normalize,
+		"equal" : URI.equal,
+		"escapeComponent" : URI.escapeComponent,
+		"unescapeComponent" : URI.unescapeComponent
+	};
+
+}());
+
+
+
+	/**
+ * JSV: JSON Schema Validator
+ * 
+ * @fileOverview A JavaScript implementation of a extendable, fully compliant JSON Schema validator.
+ * @author <a href="mailto:gary.court@gmail.com">Gary Court</a>
+ * @version 3.5
+ * @see http://github.com/garycourt/JSV
+ */
+
+/*
+ * Copyright 2010 Gary Court. 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 GARY COURT ``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 GARY COURT 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.
+ * 
+ * The views and conclusions contained in the software and documentation are those of the
+ * authors and should not be interpreted as representing official policies, either expressed
+ * or implied, of Gary Court or the JSON Schema specification.
+ */
+
+/*jslint white: true, sub: true, onevar: true, undef: true, eqeqeq: true, newcap: true, immed: true, indent: 4 */
+
+var exports = exports || this,
+	require = require || function () {
+		return exports;
+	};
+
+(function () {
+
+	var URI = require("./uri/uri").URI,
+		O = {},
+		I2H = "0123456789abcdef".split(""),
+		mapArray, filterArray, searchArray,
+
+		JSV;
+
+	//
+	// Utility functions
+	//
+
+	function typeOf(o) {
+		return o === undefined ? "undefined" : (o === null ? "null" : Object.prototype.toString.call(o).split(" ").pop().split("]").shift().toLowerCase());
+	}
+
+	/** @inner */
+	function F() {}
+
+	function createObject(proto) {
+		F.prototype = proto || {};
+		return new F();
+	}
+
+	function mapObject(obj, func, scope) {
+		var newObj = {}, key;
+		for (key in obj) {
+			if (obj[key] !== O[key]) {
+				newObj[key] = func.call(scope, obj[key], key, obj);
+			}
+		}
+		return newObj;
+	}
+
+	/** @ignore */
+	mapArray = function (arr, func, scope) {
+		var x = 0, xl = arr.length, newArr = new Array(xl);
+		for (; x < xl; ++x) {
+			newArr[x] = func.call(scope, arr[x], x, arr);
+		}
+		return newArr;
+	};
+
+	if (Array.prototype.map) {
+		/** @ignore */
+		mapArray = function (arr, func, scope) {
+			return Array.prototype.map.call(arr, func, scope);
+		};
+	}
+
+	/** @ignore */
+	filterArray = function (arr, func, scope) {
+		var x = 0, xl = arr.length, newArr = [];
+		for (; x < xl; ++x) {
+			if (func.call(scope, arr[x], x, arr)) {
+				newArr[newArr.length] = arr[x];
+			}
+		}
+		return newArr;
+	};
+
+	if (Array.prototype.filter) {
+		/** @ignore */
+		filterArray = function (arr, func, scope) {
+			return Array.prototype.filter.call(arr, func, scope);
+		};
+	}
+
+	/** @ignore */
+	searchArray = function (arr, o) {
+		var x = 0, xl = arr.length;
+		for (; x < xl; ++x) {
+			if (arr[x] === o) {
+				return x;
+			}
+		}
+		return -1;
+	};
+
+	if (Array.prototype.indexOf) {
+		/** @ignore */
+		searchArray = function (arr, o) {
+			return Array.prototype.indexOf.call(arr, o);
+		};
+	}
+
+	function toArray(o) {
+		return o !== undefined && o !== null ? (o instanceof Array && !o.callee ? o : (typeof o.length !== "number" || o.split || o.setInterval || o.call ? [ o ] : Array.prototype.slice.call(o))) : [];
+	}
+
+	function keys(o) {
+		var result = [], key;
+
+		switch (typeOf(o)) {
+		case "object":
+			for (key in o) {
+				if (o[key] !== O[key]) {
+					result[result.length] = key;
+				}
+			}
+			break;
+		case "array":
+			for (key = o.length - 1; key >= 0; --key) {
+				result[key] = key;
+			}
+			break;
+		}
+
+		return result;
+	}
+
+	function pushUnique(arr, o) {
+		if (searchArray(arr, o) === -1) {
+			arr.push(o);
+		}
+		return arr;
+	}
+
+	function popFirst(arr, o) {
+		var index = searchArray(arr, o);
+		if (index > -1) {
+			arr.splice(index, 1);
+		}
+		return arr;
+	}
+
+	function randomUUID() {
+		return [
+			I2H[Math.floor(Math.random() * 0x10)],
+			I2H[Math.floor(Math.random() * 0x10)],
+			I2H[Math.floor(Math.random() * 0x10)],
+			I2H[Math.floor(Math.random() * 0x10)],
+			I2H[Math.floor(Math.random() * 0x10)],
+			I2H[Math.floor(Math.random() * 0x10)],
+			I2H[Math.floor(Math.random() * 0x10)],
+			I2H[Math.floor(Math.random() * 0x10)],
+			"-",
+			I2H[Math.floor(Math.random() * 0x10)],
+			I2H[Math.floor(Math.random() * 0x10)],
+			I2H[Math.floor(Math.random() * 0x10)],
+			I2H[Math.floor(Math.random() * 0x10)],
+			"-4",  //set 4 high bits of time_high field to version
+			I2H[Math.floor(Math.random() * 0x10)],
+			I2H[Math.floor(Math.random() * 0x10)],
+			I2H[Math.floor(Math.random() * 0x10)],
+			"-",
+			I2H[(Math.floor(Math.random() * 0x10) & 0x3) | 0x8],  //specify 2 high bits of clock sequence
+			I2H[Math.floor(Math.random() * 0x10)],
+			I2H[Math.floor(Math.random() * 0x10)],
+			I2H[Math.floor(Math.random() * 0x10)],
+			"-",
+			I2H[Math.floor(Math.random() * 0x10)],
+			I2H[Math.floor(Math.random() * 0x10)],
+			I2H[Math.floor(Math.random() * 0x10)],
+			I2H[Math.floor(Math.random() * 0x10)],
+			I2H[Math.floor(Math.random() * 0x10)],
+			I2H[Math.floor(Math.random() * 0x10)],
+			I2H[Math.floor(Math.random() * 0x10)],
+			I2H[Math.floor(Math.random() * 0x10)],
+			I2H[Math.floor(Math.random() * 0x10)],
+			I2H[Math.floor(Math.random() * 0x10)],
+			I2H[Math.floor(Math.random() * 0x10)],
+			I2H[Math.floor(Math.random() * 0x10)]
+		].join("");
+	}
+
+	function escapeURIComponent(str) {
+		return encodeURIComponent(str).replace(/!/g, '%21').replace(/'/g, '%27').replace(/\(/g, '%28').replace(/\)/g, '%29').replace(/\*/g, '%2A');
+	}
+
+	function formatURI(uri) {
+		if (typeof uri === "string" && uri.indexOf("#") === -1) {
+			uri += "#";
+		}
+		return uri;
+	}
+
+	/**
+	 * Defines an error, found by a schema, with an instance.
+	 * This class can only be instantiated by {@link Report#addError}. 
+	 * 
+	 * @name ValidationError
+	 * @class
+	 * @see Report#addError
+	 */
+
+	/**
+	 * The URI of the instance that has the error.
+	 * 
+	 * @name ValidationError.prototype.uri
+	 * @type String
+	 */
+
+	/**
+	 * The URI of the schema that generated the error.
+	 * 
+	 * @name ValidationError.prototype.schemaUri
+	 * @type String
+	 */
+
+	/**
+	 * The name of the schema attribute that generated the error.
+	 * 
+	 * @name ValidationError.prototype.attribute
+	 * @type String
+	 */
+
+	/**
+	 * An user-friendly (English) message about what failed to validate.
+	 * 
+	 * @name ValidationError.prototype.message
+	 * @type String
+	 */
+
+	/**
+	 * The value of the schema attribute that generated the error.
+	 * 
+	 * @name ValidationError.prototype.details
+	 * @type Any
+	 */
+
+	/**
+	 * Reports are returned from validation methods to describe the result of a validation.
+	 * 
+	 * @name Report
+	 * @class
+	 * @see JSONSchema#validate
+	 * @see Environment#validate
+	 */
+
+	function Report() {
+		/**
+		 * An array of {@link ValidationError} objects that define all the errors generated by the schema against the instance.
+		 * 
+		 * @name Report.prototype.errors
+		 * @type Array
+		 * @see Report#addError
+		 */
+		this.errors = [];
+
+		/**
+		 * A hash table of every instance and what schemas were validated against it.
+		 * <p>
+		 * The key of each item in the table is the URI of the instance that was validated.
+		 * The value of this key is an array of strings of URIs of the schema that validated it.
+		 * </p>
+		 * 
+		 * @name Report.prototype.validated
+		 * @type Object
+		 * @see Report#registerValidation
+		 * @see Report#isValidatedBy
+		 */
+		this.validated = {};
+
+		/**
+		 * If the report is generated by {@link Environment#validate}, this field is the generated instance.
+		 * 
+		 * @name Report.prototype.instance
+		 * @type JSONInstance
+		 * @see Environment#validate
+		 */
+
+		/**
+		 * If the report is generated by {@link Environment#validate}, this field is the generated schema.
+		 * 
+		 * @name Report.prototype.schema
+		 * @type JSONSchema
+		 * @see Environment#validate
+		 */
+
+		/**
+		 * If the report is generated by {@link Environment#validate}, this field is the schema's schema.
+		 * This value is the same as calling <code>schema.getSchema()</code>.
+		 * 
+		 * @name Report.prototype.schemaSchema
+		 * @type JSONSchema
+		 * @see Environment#validate
+		 * @see JSONSchema#getSchema
+		 */
+	}
+
+	/**
+	 * Adds a {@link ValidationError} object to the <a href="#errors"><code>errors</code></a> field.
+	 * 
+	 * @param {JSONInstance|String} instance The instance (or instance URI) that is invalid
+	 * @param {JSONSchema|String} schema The schema (or schema URI) that was validating the instance
+	 * @param {String} attr The attribute that failed to validated
+	 * @param {String} message A user-friendly message on why the schema attribute failed to validate the instance
+	 * @param {Any} details The value of the schema attribute
+	 */
+
+	Report.prototype.addError = function (instance, schema, attr, message, details) {
+		this.errors.push({
+			uri : instance instanceof JSONInstance ? instance.getURI() : instance,
+			schemaUri : schema instanceof JSONInstance ? schema.getURI() : schema,
+			attribute : attr,
+			message : message,
+			details : details
+		});
+	};
+
+	/**
+	 * Registers that the provided instance URI has been validated by the provided schema URI. 
+	 * This is recorded in the <a href="#validated"><code>validated</code></a> field.
+	 * 
+	 * @param {String} uri The URI of the instance that was validated
+	 * @param {String} schemaUri The URI of the schema that validated the instance
+	 */
+
+	Report.prototype.registerValidation = function (uri, schemaUri) {
+		if (!this.validated[uri]) {
+			this.validated[uri] = [ schemaUri ];
+		} else {
+			this.validated[uri].push(schemaUri);
+		}
+	};
+
+	/**
+	 * Returns if an instance with the provided URI has been validated by the schema with the provided URI. 
+	 * 
+	 * @param {String} uri The URI of the instance
+	 * @param {String} schemaUri The URI of a schema
+	 * @returns {Boolean} If the instance has been validated by the schema.
+	 */
+
+	Report.prototype.isValidatedBy = function (uri, schemaUri) {
+		return !!this.validated[uri] && searchArray(this.validated[uri], schemaUri) !== -1;
+	};
+
+	/**
+	 * A wrapper class for binding an Environment, URI and helper methods to an instance. 
+	 * This class is most commonly instantiated with {@link Environment#createInstance}.
+	 * 
+	 * @name JSONInstance
+	 * @class
+	 * @param {Environment} env The environment this instance belongs to
+	 * @param {JSONInstance|Any} json The value of the instance
+	 * @param {String} [uri] The URI of the instance. If undefined, the URI will be a randomly generated UUID. 
+	 * @param {String} [fd] The fragment delimiter for properties. If undefined, uses the environment default.
+	 */
+
+	function JSONInstance(env, json, uri, fd) {
+		if (json instanceof JSONInstance) {
+			if (typeof fd !== "string") {
+				fd = json._fd;
+			}
+			if (typeof uri !== "string") {
+				uri = json._uri;
+			}
+			json = json._value;
+		}
+
+		if (typeof uri !== "string") {
+			uri = "urn:uuid:" + randomUUID() + "#";
+		} else if (uri.indexOf(":") === -1) {
+			uri = formatURI(URI.resolve("urn:uuid:" + randomUUID() + "#", uri));
+		}
+
+		this._env = env;
+		this._value = json;
+		this._uri = uri;
+		this._fd = fd || this._env._options["defaultFragmentDelimiter"];
+	}
+
+	/**
+	 * Returns the environment the instance is bound to.
+	 * 
+	 * @returns {Environment} The environment of the instance
+	 */
+
+	JSONInstance.prototype.getEnvironment = function () {
+		return this._env;
+	};
+
+	/**
+	 * Returns the name of the type of the instance.
+	 * 
+	 * @returns {String} The name of the type of the instance
+	 */
+
+	JSONInstance.prototype.getType = function () {
+		return typeOf(this._value);
+	};
+
+	/**
+	 * Returns the JSON value of the instance.
+	 * 
+	 * @returns {Any} The actual JavaScript value of the instance
+	 */
+
+	JSONInstance.prototype.getValue = function () {
+		return this._value;
+	};
+
+	/**
+	 * Returns the URI of the instance.
+	 * 
+	 * @returns {String} The URI of the instance
+	 */
+
+	JSONInstance.prototype.getURI = function () {
+		return this._uri;
+	};
+
+	/**
+	 * Returns a resolved URI of a provided relative URI against the URI of the instance.
+	 * 
+	 * @param {String} uri The relative URI to resolve
+	 * @returns {String} The resolved URI
+	 */
+
+	JSONInstance.prototype.resolveURI = function (uri) {
+		return formatURI(URI.resolve(this._uri, uri));
+	};
+
+	/**
+	 * Returns an array of the names of all the properties.
+	 * 
+	 * @returns {Array} An array of strings which are the names of all the properties
+	 */
+
+	JSONInstance.prototype.getPropertyNames = function () {
+		return keys(this._value);
+	};
+
+	/**
+	 * Returns a {@link JSONInstance} of the value of the provided property name. 
+	 * 
+	 * @param {String} key The name of the property to fetch
+	 * @returns {JSONInstance} The instance of the property value
+	 */
+
+	JSONInstance.prototype.getProperty = function (key) {
+		var value = this._value ? this._value[key] : undefined;
+		if (value instanceof JSONInstance) {
+			return value;
+		}
+		//else
+		return new JSONInstance(this._env, value, this._uri + this._fd + escapeURIComponent(key), this._fd);
+	};
+
+	/**
+	 * Returns all the property instances of the target instance.
+	 * <p>
+	 * If the target instance is an Object, then the method will return a hash table of {@link JSONInstance}s of all the properties. 
+	 * If the target instance is an Array, then the method will return an array of {@link JSONInstance}s of all the items.
+	 * </p> 
+	 * 
+	 * @returns {Object|Array|undefined} The list of instances for all the properties
+	 */
+
+	JSONInstance.prototype.getProperties = function () {
+		var type = typeOf(this._value),
+			self = this;
+
+		if (type === "object") {
+			return mapObject(this._value, function (value, key) {
+				if (value instanceof JSONInstance) {
+					return value;
+				}
+				return new JSONInstance(self._env, value, self._uri + self._fd + escapeURIComponent(key), self._fd);
+			});
+		} else if (type === "array") {
+			return mapArray(this._value, function (value, key) {
+				if (value instanceof JSONInstance) {
+					return value;
+				}
+				return new JSONInstance(self._env, value, self._uri + self._fd + escapeURIComponent(key), self._fd);
+			});
+		}
+	};
+
+	/**
+	 * Returns the JSON value of the provided property name. 
+	 * This method is a faster version of calling <code>instance.getProperty(key).getValue()</code>.
+	 * 
+	 * @param {String} key The name of the property
+	 * @returns {Any} The JavaScript value of the instance
+	 * @see JSONInstance#getProperty
+	 * @see JSONInstance#getValue
+	 */
+
+	JSONInstance.prototype.getValueOfProperty = function (key) {
+		if (this._value) {
+			if (this._value[key] instanceof JSONInstance) {
+				return this._value[key]._value;
+			}
+			return this._value[key];
+		}
+	};
+
+	/**
+	 * Return if the provided value is the same as the value of the instance.
+	 * 
+	 * @param {JSONInstance|Any} instance The value to compare
+	 * @returns {Boolean} If both the instance and the value match
+	 */
+
+	JSONInstance.prototype.equals = function (instance) {
+		if (instance instanceof JSONInstance) {
+			return this._value === instance._value;
+		}
+		//else
+		return this._value === instance;
+	};
+
+	/**
+	 * Warning: Not a generic clone function
+	 * Produces a JSV acceptable clone
+	 */
+
+	function clone(obj, deep) {
+		var newObj, x;
+
+		if (obj instanceof JSONInstance) {
+			obj = obj.getValue();
+		}
+
+		switch (typeOf(obj)) {
+		case "object":
+			if (deep) {
+				newObj = {};
+				for (x in obj) {
+					if (obj[x] !== O[x]) {
+						newObj[x] = clone(obj[x], deep);
+					}
+				}
+				return newObj;
+			} else {
+				return createObject(obj);
+			}
+			break;
+		case "array":
+			if (deep) {
+				newObj = new Array(obj.length);
+				x = obj.length;
+				while (--x >= 0) {
+					newObj[x] = clone(obj[x], deep);
+				}
+				return newObj;
+			} else {
+				return Array.prototype.slice.call(obj);
+			}
+			break;
+		default:
+			return obj;
+		}
+	}
+
+	/**
+	 * This class binds a {@link JSONInstance} with a {@link JSONSchema} to provided context aware methods. 
+	 * 
+	 * @name JSONSchema
+	 * @class
+	 * @param {Environment} env The environment this schema belongs to
+	 * @param {JSONInstance|Any} json The value of the schema
+	 * @param {String} [uri] The URI of the schema. If undefined, the URI will be a randomly generated UUID. 
+	 * @param {JSONSchema|Boolean} [schema] The schema to bind to the instance. If <code>undefined</code>, the environment's default schema will be used. If <code>true</code>, the instance's schema will be itself.
+	 * @extends JSONInstance
+	 */
+
+	function JSONSchema(env, json, uri, schema) {
+		var fr;
+		JSONInstance.call(this, env, json, uri);
+
+		if (schema === true) {
+			this._schema = this;
+		} else if (json instanceof JSONSchema && !(schema instanceof JSONSchema)) {
+			this._schema = json._schema;  //TODO: Make sure cross environments don't mess everything up
+		} else {
+			this._schema = schema instanceof JSONSchema ? schema : this._env.getDefaultSchema() || JSONSchema.createEmptySchema(this._env);
+		}
+
+		//determine fragment delimiter from schema
+		fr = this._schema.getValueOfProperty("fragmentResolution");
+		if (fr === "dot-delimited") {
+			this._fd = ".";
+		} else if (fr === "slash-delimited") {
+			this._fd = "/";
+		}
+	}
+
+	JSONSchema.prototype = createObject(JSONInstance.prototype);
+
+	/**
+	 * Creates an empty schema.
+	 * 
+	 * @param {Environment} env The environment of the schema
+	 * @returns {JSONSchema} The empty schema, who's schema is itself.
+	 */
+
+	JSONSchema.createEmptySchema = function (env) {
+		var schema = createObject(JSONSchema.prototype);
+		JSONInstance.call(schema, env, {}, undefined, undefined);
+		schema._schema = schema;
+		return schema;
+	};
+
+	/**
+	 * Returns the schema of the schema.
+	 * 
+	 * @returns {JSONSchema} The schema of the schema
+	 */
+
+	JSONSchema.prototype.getSchema = function () {
+		return this._schema;
+	};
+
+	/**
+	 * Returns the value of the provided attribute name.
+	 * <p>
+	 * This method is different from {@link JSONInstance#getProperty} as the named property 
+	 * is converted using a parser defined by the schema's schema before being returned. This
+	 * makes the return value of this method attribute dependent.
+	 * </p>
+	 * 
+	 * @param {String} key The name of the attribute
+	 * @param {Any} [arg] Some attribute parsers accept special arguments for returning resolved values. This is attribute dependent.
+	 * @returns {JSONSchema|Any} The value of the attribute
+	 */
+
+	JSONSchema.prototype.getAttribute = function (key, arg) {
+		var schemaProperty, parser, property, result;
+
+		if (!arg && this._attributes && this._attributes.hasOwnProperty(key)) {
+			return this._attributes[key];
+		}
+
+		schemaProperty = this._schema.getProperty("properties").getProperty(key);
+		parser = schemaProperty.getValueOfProperty("parser");
+		property = this.getProperty(key);
+		if (typeof parser === "function") {
+			result = parser(property, schemaProperty, arg);
+			if (!arg && this._attributes) {
+				this._attributes[key] = result;
+			}
+			return result;
+		}
+		//else
+		return property.getValue();
+	};
+
+	/**
+	 * Returns all the attributes of the schema.
+	 * 
+	 * @returns {Object} A map of all parsed attribute values
+	 */
+
+	JSONSchema.prototype.getAttributes = function () {
+		var properties, schemaProperties, key, schemaProperty, parser;
+
+		if (!this._attributes && this.getType() === "object") {
+			properties = this.getProperties();
+			schemaProperties = this._schema.getProperty("properties");
+			this._attributes = {};
+			for (key in properties) {
+				if (properties[key] !== O[key]) {
+					schemaProperty = schemaProperties && schemaProperties.getProperty(key);
+					parser = schemaProperty && schemaProperty.getValueOfProperty("parser");
+					if (typeof parser === "function") {
+						this._attributes[key] = parser(properties[key], schemaProperty);
+					} else {
+						this._attributes[key] = properties[key].getValue();
+					}
+				}
+			}
+		}
+
+		return clone(this._attributes, false);
+	};
+
+	/**
+	 * Convenience method for retrieving a link or link object from a schema. 
+	 * This method is the same as calling <code>schema.getAttribute("links", [rel, instance])[0];</code>.
+	 * 
+	 * @param {String} rel The link relationship
+	 * @param {JSONInstance} [instance] The instance to resolve any URIs from
+	 * @returns {String|Object|undefined} If <code>instance</code> is provided, a string containing the resolve URI of the link is returned.
+	 *   If <code>instance</code> is not provided, a link object is returned with details of the link.
+	 *   If no link with the provided relationship exists, <code>undefined</code> is returned.
+	 * @see JSONSchema#getAttribute
+	 */
+
+	JSONSchema.prototype.getLink = function (rel, instance) {
+		var schemaLinks = this.getAttribute("links", [rel, instance]);
+		if (schemaLinks && schemaLinks.length && schemaLinks[schemaLinks.length - 1]) {
+			return schemaLinks[schemaLinks.length - 1];
+		}
+	};
+
+	/**
+	 * Validates the provided instance against the target schema and returns a {@link Report}.
+	 * 
+	 * @param {JSONInstance|Any} instance The instance to validate; may be a {@link JSONInstance} or any JavaScript value
+	 * @param {Report} [report] A {@link Report} to concatenate the result of the validation to. If <code>undefined</code>, a new {@link Report} is created. 
+	 * @param {JSONInstance} [parent] The parent/containing instance of the provided instance
+	 * @param {JSONSchema} [parentSchema] The schema of the parent/containing instance
+	 * @param {String} [name] The name of the parent object's property that references the instance
+	 * @returns {Report} The result of the validation
+	 */
+
+	JSONSchema.prototype.validate = function (instance, report, parent, parentSchema, name) {
+		var validator = this._schema.getValueOfProperty("validator");
+
+		if (!(instance instanceof JSONInstance)) {
+			instance = this.getEnvironment().createInstance(instance);
+		}
+
+		if (!(report instanceof Report)) {
+			report = new Report();
+		}
+
+		if (typeof validator === "function" && !report.isValidatedBy(instance.getURI(), this.getURI())) {
+			report.registerValidation(instance.getURI(), this.getURI());
+			validator(instance, this, this._schema, report, parent, parentSchema, name);
+		}
+
+		return report;
+	};
+
+	/**
+	 * Merges two schemas/instances together.
+	 */
+
+	function inherits(base, extra, extension) {
+		var baseType = typeOf(base),
+			extraType = typeOf(extra),
+			child, x;
+
+		if (extraType === "undefined") {
+			return clone(base, true);
+		} else if (baseType === "undefined" || extraType !== baseType) {
+			return clone(extra, true);
+		} else if (extraType === "object") {
+			if (base instanceof JSONSchema) {
+				base = base.getAttributes();
+			}
+			if (extra instanceof JSONSchema) {
+				extra = extra.getAttributes();
+				if (extra["extends"] && extension && extra["extends"] instanceof JSONSchema) {
+					extra["extends"] = [ extra["extends"] ];
+				}
+			}
+			child = clone(base, true);  //this could be optimized as some properties get overwritten
+			for (x in extra) {
+				if (extra[x] !== O[x]) {
+					child[x] = inherits(base[x], extra[x], extension);
+				}
+			}
+			return child;
+		} else {
+			return clone(extra, true);
+		}
+	}
+
+	/**
+	 * An Environment is a sandbox of schemas thats behavior is different from other environments.
+	 * 
+	 * @name Environment
+	 * @class
+	 */
+
+	function Environment() {
+		this._id = randomUUID();
+		this._schemas = {};
+		this._options = {};
+	}
+
+	/**
+	 * Returns a clone of the target environment.
+	 * 
+	 * @returns {Environment} A new {@link Environment} that is a exact copy of the target environment 
+	 */
+
+	Environment.prototype.clone = function () {
+		var env = new Environment();
+		env._schemas = createObject(this._schemas);
+		env._options = createObject(this._options);
+
+		return env;
+	};
+
+	/**
+	 * Returns a new {@link JSONInstance} of the provided data.
+	 * 
+	 * @param {JSONInstance|Any} data The value of the instance
+	 * @param {String} [uri] The URI of the instance. If undefined, the URI will be a randomly generated UUID. 
+	 * @returns {JSONInstance} A new {@link JSONInstance} from the provided data
+	 */
+
+	Environment.prototype.createInstance = function (data, uri) {
+		var instance;
+		uri = formatURI(uri);
+
+		if (data instanceof JSONInstance && (!uri || data.getURI() === uri)) {
+			return data;
+		}
+		//else
+		instance = new JSONInstance(this, data, uri);
+
+		return instance;
+	};
+
+	/**
+	 * Creates a new {@link JSONSchema} from the provided data, and registers it with the environment. 
+	 * 
+	 * @param {JSONInstance|Any} data The value of the schema
+	 * @param {JSONSchema|Boolean} [schema] The schema to bind to the instance. If <code>undefined</code>, the environment's default schema will be used. If <code>true</code>, the instance's schema will be itself.
+	 * @param {String} [uri] The URI of the schema. If undefined, the URI will be a randomly generated UUID. 
+	 * @returns {JSONSchema} A new {@link JSONSchema} from the provided data
+	 * @throws {InitializationError} If a schema that is not registered with the environment is referenced 
+	 */
+
+	Environment.prototype.createSchema = function (data, schema, uri) {
+		var instance, 
+			initializer;
+		uri = formatURI(uri);
+
+		if (data instanceof JSONSchema && (!uri || data._uri === uri) && (!schema || data._schema.equals(schema))) {
+			return data;
+		}
+
+		instance = new JSONSchema(this, data, uri, schema);
+
+		initializer = instance.getSchema().getValueOfProperty("initializer");
+		if (typeof initializer === "function") {
+			instance = initializer(instance);
+		}
+
+		//register schema
+		this._schemas[instance._uri] = instance;
+		this._schemas[uri] = instance;
+
+		//build & cache the rest of the schema
+		instance.getAttributes();
+
+		return instance;
+	};
+
+	/**
+	 * Creates an empty schema.
+	 * 
+	 * @param {Environment} env The environment of the schema
+	 * @returns {JSONSchema} The empty schema, who's schema is itself.
+	 */
+
+	Environment.prototype.createEmptySchema = function () {
+		var schema = JSONSchema.createEmptySchema(this);
+		this._schemas[schema._uri] = schema;
+		return schema;
+	};
+
+	/**
+	 * Returns the schema registered with the provided URI.
+	 * 
+	 * @param {String} uri The absolute URI of the required schema
+	 * @returns {JSONSchema|undefined} The request schema, or <code>undefined</code> if not found
+	 */
+
+	Environment.prototype.findSchema = function (uri) {
+		return this._schemas[formatURI(uri)];
+	};
+
+	/**
+	 * Sets the specified environment option to the specified value.
+	 * 
+	 * @param {String} name The name of the environment option to set
+	 * @param {Any} value The new value of the environment option
+	 */
+
+	Environment.prototype.setOption = function (name, value) {
+		this._options[name] = value;
+	};
+
+	/**
+	 * Returns the specified environment option.
+	 * 
+	 * @param {String} name The name of the environment option to set
+	 * @returns {Any} The value of the environment option
+	 */
+
+	Environment.prototype.getOption = function (name) {
+		return this._options[name];
+	};
+
+	/**
+	 * Sets the default fragment delimiter of the environment.
+	 * 
+	 * @deprecated Use {@link Environment#setOption} with option "defaultFragmentDelimiter"
+	 * @param {String} fd The fragment delimiter character
+	 */
+
+	Environment.prototype.setDefaultFragmentDelimiter = function (fd) {
+		if (typeof fd === "string" && fd.length > 0) {
+			this._options["defaultFragmentDelimiter"] = fd;
+		}
+	};
+
+	/**
+	 * Returns the default fragment delimiter of the environment.
+	 * 
+	 * @deprecated Use {@link Environment#getOption} with option "defaultFragmentDelimiter"
+	 * @returns {String} The fragment delimiter character
+	 */
+
+	Environment.prototype.getDefaultFragmentDelimiter = function () {
+		return this._options["defaultFragmentDelimiter"];
+	};
+
+	/**
+	 * Sets the URI of the default schema for the environment.
+	 * 
+	 * @deprecated Use {@link Environment#setOption} with option "defaultSchemaURI"
+	 * @param {String} uri The default schema URI
+	 */
+
+	Environment.prototype.setDefaultSchemaURI = function (uri) {
+		if (typeof uri === "string") {
+			this._options["defaultSchemaURI"] = formatURI(uri);
+		}
+	};
+
+	/**
+	 * Returns the default schema of the environment.
+	 * 
+	 * @returns {JSONSchema} The default schema
+	 */
+
+	Environment.prototype.getDefaultSchema = function () {
+		return this.findSchema(this._options["defaultSchemaURI"]);
+	};
+
+	/**
+	 * Validates both the provided schema and the provided instance, and returns a {@link Report}. 
+	 * If the schema fails to validate, the instance will not be validated.
+	 * 
+	 * @param {JSONInstance|Any} instanceJSON The {@link JSONInstance} or JavaScript value to validate.
+	 * @param {JSONSchema|Any} schemaJSON The {@link JSONSchema} or JavaScript value to use in the validation. This will also be validated againt the schema's schema.
+	 * @returns {Report} The result of the validation
+	 */
+
+	Environment.prototype.validate = function (instanceJSON, schemaJSON) {
+		var instance,
+			schema,
+			schemaSchema,
+			report = new Report();
+
+		try {
+			instance = this.createInstance(instanceJSON);
+			report.instance = instance;
+		} catch (e) {
+			report.addError(e.uri, e.schemaUri, e.attribute, e.message, e.details);
+		}
+
+		try {
+			schema = this.createSchema(schemaJSON);
+			report.schema = schema;
+
+			schemaSchema = schema.getSchema();
+			report.schemaSchema = schemaSchema;
+		} catch (e) {
+			report.addError(e.uri, e.schemaUri, e.attribute, e.message, e.details);
+		}
+
+		if (schemaSchema) {
+			schemaSchema.validate(schema, report);
+		}
+
+		if (report.errors.length) {
+			return report;
+		}
+
+		return schema.validate(instance, report);
+	};
+
+	/**
+	 * @private
+	 */
+
+	Environment.prototype._checkForInvalidInstances = function (stackSize, schemaURI) {
+		var result = [],
+			stack = [
+				[schemaURI, this._schemas[schemaURI]]
+			], 
+			counter = 0,
+			item, uri, instance, schema, properties, key;
+
+		while (counter++ < stackSize && stack.length) {
+			item = stack.shift();
+			uri = item[0];
+			instance = item[1];
+
+			if (instance instanceof JSONSchema) {
+				if (this._schemas[instance._uri] !== instance) {
+					result.push("Instance " + uri + " does not match " + instance._uri);
+				} else {
+					//schema = instance.getSchema();
+					//stack.push([uri + "/{schema}", schema]);
+
+					properties = instance.getAttributes();
+					for (key in properties) {
+						if (properties[key] !== O[key]) {
+							stack.push([uri + "/" + escapeURIComponent(key), properties[key]]);
+						}
+					}
+				}
+			} else if (typeOf(instance) === "object") {
+				properties = instance;
+				for (key in properties) {
+					if (properties.hasOwnProperty(key)) {
+						stack.push([uri + "/" + escapeURIComponent(key), properties[key]]);
+					}
+				}
+			} else if (typeOf(instance) === "array") {
+				properties = instance;
+				for (key = 0; key < properties.length; ++key) {
+					stack.push([uri + "/" + escapeURIComponent(key), properties[key]]);
+				}
+			}
+		}
+
+		return result.length ? result : counter;
+	};
+
+	/**
+	 * A globaly accessible object that provides the ability to create and manage {@link Environments},
+	 * as well as providing utility methods.
+	 * 
+	 * @namespace
+	 */
+
+	JSV = {
+		_environments : {},
+		_defaultEnvironmentID : "",
+
+		/**
+		 * Returns if the provide value is an instance of {@link JSONInstance}.
+		 * 
+		 * @param o The value to test
+		 * @returns {Boolean} If the provide value is an instance of {@link JSONInstance}
+		 */
+
+		isJSONInstance : function (o) {
+			return o instanceof JSONInstance;
+		},
+
+		/**
+		 * Returns if the provide value is an instance of {@link JSONSchema}.
+		 * 
+		 * @param o The value to test
+		 * @returns {Boolean} If the provide value is an instance of {@link JSONSchema}
+		 */
+
+		isJSONSchema : function (o) {
+			return o instanceof JSONSchema;
+		},
+
+		/**
+		 * Creates and returns a new {@link Environment} that is a clone of the environment registered with the provided ID.
+		 * If no environment ID is provided, the default environment is cloned.
+		 * 
+		 * @param {String} [id] The ID of the environment to clone. If <code>undefined</code>, the default environment ID is used.
+		 * @returns {Environment} A newly cloned {@link Environment}
+		 * @throws {Error} If there is no environment registered with the provided ID
+		 */
+
+		createEnvironment : function (id) {
+			id = id || this._defaultEnvironmentID;
+
+			if (!this._environments[id]) {
+				throw new Error("Unknown Environment ID");
+			}
+			//else
+			return this._environments[id].clone();
+		},
+
+		Environment : Environment,
+
+		/**
+		 * Registers the provided {@link Environment} with the provided ID.
+		 * 
+		 * @param {String} id The ID of the environment
+		 * @param {Environment} env The environment to register
+		 */
+
+		registerEnvironment : function (id, env) {
+			id = id || (env || 0)._id;
+			if (id && !this._environments[id] && env instanceof Environment) {
+				env._id = id;
+				this._environments[id] = env;
+			}
+		},
+
+		/**
+		 * Sets which registered ID is the default environment.
+		 * 
+		 * @param {String} id The ID of the registered environment that is default
+		 * @throws {Error} If there is no registered environment with the provided ID
+		 */
+
+		setDefaultEnvironmentID : function (id) {
+			if (typeof id === "string") {
+				if (!this._environments[id]) {
+					throw new Error("Unknown Environment ID");
+				}
+
+				this._defaultEnvironmentID = id;
+			}
+		},
+
+		/**
+		 * Returns the ID of the default environment.
+		 * 
+		 * @returns {String} The ID of the default environment
+		 */
+
+		getDefaultEnvironmentID : function () {
+			return this._defaultEnvironmentID;
+		},
+
+		//
+		// Utility Functions
+		//
+
+		/**
+		 * Returns the name of the type of the provided value.
+		 *
+		 * @event //utility
+		 * @param {Any} o The value to determine the type of
+		 * @returns {String} The name of the type of the value
+		 */
+		typeOf : typeOf,
+
+		/**
+		 * Return a new object that inherits all of the properties of the provided object.
+		 *
+		 * @event //utility
+		 * @param {Object} proto The prototype of the new object
+		 * @returns {Object} A new object that inherits all of the properties of the provided object
+		 */
+		createObject : createObject,
+
+		/**
+		 * Returns a new object with each property transformed by the iterator.
+		 *
+		 * @event //utility
+		 * @param {Object} obj The object to transform
+		 * @param {Function} iterator A function that returns the new value of the provided property
+		 * @param {Object} [scope] The value of <code>this</code> in the iterator
+		 * @returns {Object} A new object with each property transformed
+		 */
+		mapObject : mapObject,
+
+		/**
+		 * Returns a new array with each item transformed by the iterator.
+		 * 
+		 * @event //utility
+		 * @param {Array} arr The array to transform
+		 * @param {Function} iterator A function that returns the new value of the provided item
+		 * @param {Object} scope The value of <code>this</code> in the iterator
+		 * @returns {Array} A new array with each item transformed
+		 */
+		mapArray : mapArray,
+
+		/**
+		 * Returns a new array that only contains the items allowed by the iterator.
+		 *
+		 * @event //utility
+		 * @param {Array} arr The array to filter
+		 * @param {Function} iterator The function that returns true if the provided property should be added to the array
+		 * @param {Object} scope The value of <code>this</code> within the iterator
+		 * @returns {Array} A new array that contains the items allowed by the iterator
+		 */
+		filterArray : filterArray,
+
+		/**
+		 * Returns the first index in the array that the provided item is located at.
+		 *
+		 * @event //utility
+		 * @param {Array} arr The array to search
+		 * @param {Any} o The item being searched for
+		 * @returns {Number} The index of the item in the array, or <code>-1</code> if not found
+		 */
+		searchArray : searchArray,
+
+		/**
+		 * Returns an array representation of a value.
+		 * <ul>
+		 * <li>For array-like objects, the value will be casted as an Array type.</li>
+		 * <li>If an array is provided, the function will simply return the same array.</li>
+		 * <li>For a null or undefined value, the result will be an empty Array.</li>
+		 * <li>For all other values, the value will be the first element in a new Array. </li>
+		 * </ul>
+		 *
+		 * @event //utility
+		 * @param {Any} o The value to convert into an array
+		 * @returns {Array} The value as an array
+		 */
+		toArray : toArray,
+
+		/**
+		 * Returns an array of the names of all properties of an object.
+		 * 
+		 * @event //utility
+		 * @param {Object|Array} o The object in question
+		 * @returns {Array} The names of all properties
+		 */
+		keys : keys,
+
+		/**
+		 * Mutates the array by pushing the provided value onto the array only if it is not already there.
+		 *
+		 * @event //utility
+		 * @param {Array} arr The array to modify
+		 * @param {Any} o The object to add to the array if it is not already there
+		 * @returns {Array} The provided array for chaining
+		 */
+		pushUnique : pushUnique,
+
+		/**
+		 * Mutates the array by removing the first item that matches the provided value in the array.
+		 *
+		 * @event //utility
+		 * @param {Array} arr The array to modify
+		 * @param {Any} o The object to remove from the array
+		 * @returns {Array} The provided array for chaining
+		 */
+		popFirst : popFirst,
+
+		/**
+		 * Creates a copy of the target object.
+		 * <p>
+		 * This method will create a new instance of the target, and then mixin the properties of the target.
+		 * If <code>deep</code> is <code>true</code>, then each property will be cloned before mixin.
+		 * </p>
+		 * <p><b>Warning</b>: This is not a generic clone function, as it will only properly clone objects and arrays.</p>
+		 * 
+		 * @event //utility
+		 * @param {Any} o The value to clone 
+		 * @param {Boolean} [deep=false] If each property should be recursively cloned
+		 * @returns A cloned copy of the provided value
+		 */
+		clone : clone,
+
+		/**
+		 * Generates a pseudo-random UUID.
+		 * 
+		 * @event //utility
+		 * @returns {String} A new universally unique ID
+		 */
+		randomUUID : randomUUID,
+
+		/**
+		 * Properly escapes a URI component for embedding into a URI string.
+		 * 
+		 * @event //utility
+		 * @param {String} str The URI component to escape
+		 * @returns {String} The escaped URI component
+		 */
+		escapeURIComponent : escapeURIComponent,
+
+		/**
+		 * Returns a URI that is formated for JSV. Currently, this only ensures that the URI ends with a hash tag (<code>#</code>).
+		 * 
+		 * @event //utility
+		 * @param {String} uri The URI to format
+		 * @returns {String} The URI formatted for JSV
+		 */
+		formatURI : formatURI,
+
+		/**
+		 * Merges two schemas/instance together.
+		 * 
+		 * @event //utility
+		 * @param {JSONSchema|Any} base The old value to merge
+		 * @param {JSONSchema|Any} extra The new value to merge
+		 * @param {Boolean} extension If the merge is a JSON Schema extension
+		 * @return {Any} The modified base value
+		 */
+
+		inherits : inherits
+	};
+
+	this.JSV = JSV;  //set global object
+	exports.JSV = JSV;  //export to CommonJS
+
+	require("./environments");  //load default environments
+
+}());
+
+
+
+
+
+/**
+ * json-schema-draft-03 Environment
+ * 
+ * @fileOverview Implementation of the third revision of the JSON Schema specification draft.
+ * @author <a href="mailto:gary.court@gmail.com">Gary Court</a>
+ * @version 1.3
+ * @see http://github.com/garycourt/JSV
+ */
+
+/*
+ * Copyright 2010 Gary Court. 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 GARY COURT ``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 GARY COURT 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.
+ * 
+ * The views and conclusions contained in the software and documentation are those of the
+ * authors and should not be interpreted as representing official policies, either expressed
+ * or implied, of Gary Court or the JSON Schema specification.
+ */
+
+/*jslint white: true, sub: true, onevar: true, undef: true, eqeqeq: true, newcap: true, immed: true, indent: 4 */
+/*global require */
+
+(function () {
+	var O = {},
+		JSV = require('./jsv').JSV,
+		InitializationError,
+		TYPE_VALIDATORS,
+		ENVIRONMENT,
+		SCHEMA_00_JSON,
+		HYPERSCHEMA_00_JSON,
+		LINKS_00_JSON, 
+		SCHEMA_00,
+		HYPERSCHEMA_00,
+		LINKS_00, 
+		SCHEMA_01_JSON,
+		HYPERSCHEMA_01_JSON,
+		LINKS_01_JSON, 
+		SCHEMA_01,
+		HYPERSCHEMA_01,
+		LINKS_01, 
+		SCHEMA_02_JSON,
+		HYPERSCHEMA_02_JSON,
+		LINKS_02_JSON,
+		SCHEMA_02,
+		HYPERSCHEMA_02,
+		LINKS_02, 
+		SCHEMA_03_JSON,
+		HYPERSCHEMA_03_JSON,
+		LINKS_03_JSON,
+		SCHEMA_03,
+		HYPERSCHEMA_03,
+		LINKS_03;
+
+	InitializationError = function InitializationError(instance, schema, attr, message, details) {
+		Error.call(this, message);
+
+		this.uri = instance.getURI();
+		this.schemaUri = schema.getURI();
+		this.attribute = attr;
+		this.message = message;
+		this.description = message;  //IE
+		this.details = details;
+	}
+	InitializationError.prototype = new Error();
+	InitializationError.prototype.constructor = InitializationError;
+	InitializationError.prototype.name = "InitializationError";
+
+	TYPE_VALIDATORS = {
+		"string" : function (instance, report) {
+			return instance.getType() === "string";
+		},
+
+		"number" : function (instance, report) {
+			return instance.getType() === "number";
+		},
+
+		"integer" : function (instance, report) {
+			return instance.getType() === "number" && instance.getValue() % 1 === 0;
+		},
+
+		"boolean" : function (instance, report) {
+			return instance.getType() === "boolean";
+		},
+
+		"object" : function (instance, report) {
+			return instance.getType() === "object";
+		},
+
+		"array" : function (instance, report) {
+			return instance.getType() === "array";
+		},
+
+		"null" : function (instance, report) {
+			return instance.getType() === "null";
+		},
+
+		"any" : function (instance, report) {
+			return true;
+		}
+	};
+
+	ENVIRONMENT = new JSV.Environment();
+	ENVIRONMENT.setOption("strict", false);
+	ENVIRONMENT.setOption("validateReferences", false);  //updated later
+
+	//
+	// draft-00
+	//
+
+	SCHEMA_00_JSON = {
+		"$schema" : "http://json-schema.org/draft-00/hyper-schema#",
+		"id" : "http://json-schema.org/draft-00/schema#",
+		"type" : "object",
+
+		"properties" : {
+			"type" : {
+				"type" : ["string", "array"],
+				"items" : {
+					"type" : ["string", {"$ref" : "#"}]
+				},
+				"optional" : true,
+				"uniqueItems" : true,
+				"default" : "any",
+
+				"parser" : function (instance, self) {
+					var parser;
+
+					if (instance.getType() === "string") {
+						return instance.getValue();
+					} else if (instance.getType() === "object") {
+						return instance.getEnvironment().createSchema(
+							instance, 
+							self.getEnvironment().findSchema(self.resolveURI("#"))
+						);
+					} else if (instance.getType() === "array") {
+						parser = self.getValueOfProperty("parser");
+						return JSV.mapArray(instance.getProperties(), function (prop) {
+							return parser(prop, self);
+						});
+					}
+					//else
+					return "any";
+				},
+
+				"validator" : function (instance, schema, self, report, parent, parentSchema, name) {
+					var requiredTypes = JSV.toArray(schema.getAttribute("type")),
+						x, xl, type, subreport, typeValidators;
+
+					//for instances that are required to be a certain type
+					if (instance.getType() !== "undefined" && requiredTypes && requiredTypes.length) {
+						typeValidators = self.getValueOfProperty("typeValidators") || {};
+
+						//ensure that type matches for at least one of the required types
+						for (x = 0, xl = requiredTypes.length; x < xl; ++x) {
+							type = requiredTypes[x];
+							if (JSV.isJSONSchema(type)) {
+								subreport = JSV.createObject(report);
+								subreport.errors = [];
+								subreport.validated = JSV.clone(report.validated);
+								if (type.validate(instance, subreport, parent, parentSchema, name).errors.length === 0) {
+									return true;  //instance matches this schema
+								}
+							} else {
+								if (typeValidators[type] !== O[type] && typeof typeValidators[type] === "function") {
+									if (typeValidators[type](instance, report)) {
+										return true;  //type is valid
+									}
+								} else {
+									return true;  //unknown types are assumed valid
+								}
+							}
+						}
+
+						//if we get to this point, type is invalid
+						report.addError(instance, schema, "type", "Instance is not a required type", requiredTypes);
+						return false;
+					}
+					//else, anything is allowed if no type is specified
+					return true;
+				},
+
+				"typeValidators" : TYPE_VALIDATORS
+			},
+
+			"properties" : {
+				"type" : "object",
+				"additionalProperties" : {"$ref" : "#"},
+				"optional" : true,
+				"default" : {},
+
+				"parser" : function (instance, self, arg) {
+					var env = instance.getEnvironment(),
+						selfEnv = self.getEnvironment();
+					if (instance.getType() === "object") {
+						if (arg) {
+							return env.createSchema(instance.getProperty(arg), selfEnv.findSchema(self.resolveURI("#")));
+						} else {
+							return JSV.mapObject(instance.getProperties(), function (instance) {
+								return env.createSchema(instance, selfEnv.findSchema(self.resolveURI("#")));
+							});
+						}
+					}
+					//else
+					return {};
+				},
+
+				"validator" : function (instance, schema, self, report, parent, parentSchema, name) {
+					var propertySchemas, key;
+					//this attribute is for object type instances only
+					if (instance.getType() === "object") {
+						//for each property defined in the schema
+						propertySchemas = schema.getAttribute("properties");
+						for (key in propertySchemas) {
+							if (propertySchemas[key] !== O[key] && propertySchemas[key]) {
+								//ensure that instance property is valid
+								propertySchemas[key].validate(instance.getProperty(key), report, instance, schema, key);
+							}
+						}
+					}
+				}
+			},
+
+			"items" : {
+				"type" : [{"$ref" : "#"}, "array"],
+				"items" : {"$ref" : "#"},
+				"optional" : true,
+				"default" : {},
+
+				"parser" : function (instance, self) {
+					if (instance.getType() === "object") {
+						return instance.getEnvironment().createSchema(instance, self.getEnvironment().findSchema(self.resolveURI("#")));
+					} else if (instance.getType() === "array") {
+						return JSV.mapArray(instance.getProperties(), function (instance) {
+							return instance.getEnvironment().createSchema(instance, self.getEnvironment().findSchema(self.resolveURI("#")));
+						});
+					}
+					//else
+					return instance.getEnvironment().createEmptySchema();
+				},
+
+				"validator" : function (instance, schema, self, report, parent, parentSchema, name) {
+					var properties, items, x, xl, itemSchema, additionalProperties;
+
+					if (instance.getType() === "array") {
+						properties = instance.getProperties();
+						items = schema.getAttribute("items");
+						additionalProperties = schema.getAttribute("additionalProperties");
+
+						if (JSV.typeOf(items) === "array") {
+							for (x = 0, xl = properties.length; x < xl; ++x) {
+								itemSchema = items[x] || additionalProperties;
+								if (itemSchema !== false) {
+									itemSchema.validate(properties[x], report, instance, schema, x);
+								} else {
+									report.addError(instance, schema, "additionalProperties", "Additional items are not allowed", itemSchema);
+								}
+							}
+						} else {
+							itemSchema = items || additionalProperties;
+							for (x = 0, xl = properties.length; x < xl; ++x) {
+								itemSchema.validate(properties[x], report, instance, schema, x);
+							}
+						}
+					}
+				}
+			},
+
+			"optional" : {
+				"type" : "boolean",
+				"optional" : true,
+				"default" : false,
+
+				"parser" : function (instance, self) {
+					return !!instance.getValue();
+				},
+
+				"validator" : function (instance, schema, self, report, parent, parentSchema, name) {
+					if (instance.getType() === "undefined" && !schema.getAttribute("optional")) {
+						report.addError(instance, schema, "optional", "Property is required", false);
+					}
+				},
+
+				"validationRequired" : true
+			},
+
+			"additionalProperties" : {
+				"type" : [{"$ref" : "#"}, "boolean"],
+				"optional" : true,
+				"default" : {},
+
+				"parser" : function (instance, self) {
+					if (instance.getType() === "object") {
+						return instance.getEnvironment().createSchema(instance, self.getEnvironment().findSchema(self.resolveURI("#")));
+					} else if (instance.getType() === "boolean" && instance.getValue() === false) {
+						return false;
+					}
+					//else
+					return instance.getEnvironment().createEmptySchema();
+				},
+
+				"validator" : function (instance, schema, self, report, parent, parentSchema, name) {
+					var additionalProperties, propertySchemas, properties, key;
+					//we only need to check against object types as arrays do their own checking on this property
+					if (instance.getType() === "object") {
+						additionalProperties = schema.getAttribute("additionalProperties");
+						propertySchemas = schema.getAttribute("properties") || {};
+						properties = instance.getProperties();
+						for (key in properties) {
+							if (properties[key] !== O[key] && properties[key] && propertySchemas[key] === O[key]) {
+								if (JSV.isJSONSchema(additionalProperties)) {
+									additionalProperties.validate(properties[key], report, instance, schema, key);
+								} else if (additionalProperties === false) {
+									report.addError(instance, schema, "additionalProperties", "Additional properties are not allowed", additionalProperties);
+								}
+							}
+						}
+					}
+				}
+			},
+
+			"requires" : {
+				"type" : ["string", {"$ref" : "#"}],
+				"optional" : true,
+
+				"parser" : function (instance, self) {
+					if (instance.getType() === "string") {
+						return instance.getValue();
+					} else if (instance.getType() === "object") {
+						return instance.getEnvironment().createSchema(instance, self.getEnvironment().findSchema(self.resolveURI("#")));
+					}
+				},
+
+				"validator" : function (instance, schema, self, report, parent, parentSchema, name) {
+					var requires;
+					if (instance.getType() !== "undefined" && parent && parent.getType() !== "undefined") {
+						requires = schema.getAttribute("requires");
+						if (typeof requires === "string") {
+							if (parent.getProperty(requires).getType() === "undefined") {
+								report.addError(instance, schema, "requires", 'Property requires sibling property "' + requires + '"', requires);
+							}
+						} else if (JSV.isJSONSchema(requires)) {
+							requires.validate(parent, report);  //WATCH: A "requires" schema does not support the "requires" attribute
+						}
+					}
+				}
+			},
+
+			"minimum" : {
+				"type" : "number",
+				"optional" : true,
+
+				"parser" : function (instance, self) {
+					if (instance.getType() === "number") {
+						return instance.getValue();
+					}
+				},
+
+				"validator" : function (instance, schema, self, report, parent, parentSchema, name) {
+					var minimum, minimumCanEqual;
+					if (instance.getType() === "number") {
+						minimum = schema.getAttribute("minimum");
+						minimumCanEqual = schema.getAttribute("minimumCanEqual");
+						if (typeof minimum === "number" && (instance.getValue() < minimum || (minimumCanEqual === false && instance.getValue() === minimum))) {
+							report.addError(instance, schema, "minimum", "Number is less then the required minimum value", minimum);
+						}
+					}
+				}
+			},
+
+			"maximum" : {
+				"type" : "number",
+				"optional" : true,
+
+				"parser" : function (instance, self) {
+					if (instance.getType() === "number") {
+						return instance.getValue();
+					}
+				},
+
+				"validator" : function (instance, schema, self, report, parent, parentSchema, name) {
+					var maximum, maximumCanEqual;
+					if (instance.getType() === "number") {
+						maximum = schema.getAttribute("maximum");
+						maximumCanEqual = schema.getAttribute("maximumCanEqual");
+						if (typeof maximum === "number" && (instance.getValue() > maximum || (maximumCanEqual === false && instance.getValue() === maximum))) {
+							report.addError(instance, schema, "maximum", "Number is greater then the required maximum value", maximum);
+						}
+					}
+				}
+			},
+
+			"minimumCanEqual" : {
+				"type" : "boolean",
+				"optional" : true,
+				"requires" : "minimum",
+				"default" : true,
+
+				"parser" : function (instance, self) {
+					if (instance.getType() === "boolean") {
+						return instance.getValue();
+					}
+					//else
+					return true;
+				}
+			},
+
+			"maximumCanEqual" : {
+				"type" : "boolean",
+				"optional" : true,
+				"requires" : "maximum",
+				"default" : true,
+
+				"parser" : function (instance, self) {
+					if (instance.getType() === "boolean") {
+						return instance.getValue();
+					}
+					//else
+					return true;
+				}
+			},
+
+			"minItems" : {
+				"type" : "integer",
+				"optional" : true,
+				"minimum" : 0,
+				"default" : 0,
+
+				"parser" : function (instance, self) {
+					if (instance.getType() === "number") {
+						return instance.getValue();
+					}
+					//else
+					return 0;
+				},
+
+				"validator" : function (instance, schema, self, report, parent, parentSchema, name) {
+					var minItems;
+					if (instance.getType() === "array") {
+						minItems = schema.getAttribute("minItems");
+						if (typeof minItems === "number" && instance.getProperties().length < minItems) {
+							report.addError(instance, schema, "minItems", "The number of items is less then the required minimum", minItems);
+						}
+					}
+				}
+			},
+
+			"maxItems" : {
+				"type" : "integer",
+				"optional" : true,
+				"minimum" : 0,
+
+				"parser" : function (instance, self) {
+					if (instance.getType() === "number") {
+						return instance.getValue();
+					}
+				},
+
+				"validator" : function (instance, schema, self, report, parent, parentSchema, name) {
+					var maxItems;
+					if (instance.getType() === "array") {
+						maxItems = schema.getAttribute("maxItems");
+						if (typeof maxItems === "number" && instance.getProperties().length > maxItems) {
+							report.addError(instance, schema, "maxItems", "The number of items is greater then the required maximum", maxItems);
+						}
+					}
+				}
+			},
+
+			"pattern" : {
+				"type" : "string",
+				"optional" : true,
+				"format" : "regex",
+
+				"parser" : function (instance, self) {
+					if (instance.getType() === "string") {
+						try {
+							return new RegExp(instance.getValue());
+						} catch (e) {
+							return e;
+						}
+					}
+				},
+
+				"validator" : function (instance, schema, self, report, parent, parentSchema, name) {
+					var pattern;
+					try {
+						pattern = schema.getAttribute("pattern");
+						if (pattern instanceof Error) {
+							report.addError(schema, self, "pattern", "Invalid pattern", schema.getValueOfProperty("pattern"));
+						} else if (instance.getType() === "string" && pattern && !pattern.test(instance.getValue())) {
+							report.addError(instance, schema, "pattern", "String does not match pattern", pattern.toString());
+						}
+					} catch (e) {
+						report.addError(schema, self, "pattern", "Invalid pattern", schema.getValueOfProperty("pattern"));
+					}
+				}
+			},
+
+			"minLength" : {
+				"type" : "integer",
+				"optional" : true,
+				"minimum" : 0,
+				"default" : 0,
+
+				"parser" : function (instance, self) {
+					if (instance.getType() === "number") {
+						return instance.getValue();
+					}
+					//else
+					return 0;
+				},
+
+				"validator" : function (instance, schema, self, report, parent, parentSchema, name) {
+					var minLength;
+					if (instance.getType() === "string") {
+						minLength = schema.getAttribute("minLength");
+						if (typeof minLength === "number" && instance.getValue().length < minLength) {
+							report.addError(instance, schema, "minLength", "String is less then the required minimum length", minLength);
+						}
+					}
+				}
+			},
+
+			"maxLength" : {
+				"type" : "integer",
+				"optional" : true,
+
+				"parser" : function (instance, self) {
+					if (instance.getType() === "number") {
+						return instance.getValue();
+					}
+				},
+
+				"validator" : function (instance, schema, self, report, parent, parentSchema, name) {
+					var maxLength;
+					if (instance.getType() === "string") {
+						maxLength = schema.getAttribute("maxLength");
+						if (typeof maxLength === "number" && instance.getValue().length > maxLength) {
+							report.addError(instance, schema, "maxLength", "String is greater then the required maximum length", maxLength);
+						}
+					}
+				}
+			},
+
+			"enum" : {
+				"type" : "array",
+				"optional" : true,
+				"minItems" : 1,
+				"uniqueItems" : true,
+
+				"parser" : function (instance, self) {
+					if (instance.getType() === "array") {
+						return instance.getValue();
+					}
+				},
+
+				"validator" : function (instance, schema, self, report, parent, parentSchema, name) {
+					var enums, x, xl;
+					if (instance.getType() !== "undefined") {
+						enums = schema.getAttribute("enum");
+						if (enums) {
+							for (x = 0, xl = enums.length; x < xl; ++x) {
+								if (instance.equals(enums[x])) {
+									return true;
+								}
+							}
+							report.addError(instance, schema, "enum", "Instance is not one of the possible values", enums);
+						}
+					}
+				}
+			},
+
+			"title" : {
+				"type" : "string",
+				"optional" : true
+			},
+
+			"description" : {
+				"type" : "string",
+				"optional" : true
+			},
+
+			"format" : {
+				"type" : "string",
+				"optional" : true,
+
+				"parser" : function (instance, self) {
+					if (instance.getType() === "string") {
+						return instance.getValue();
+					}
+				},
+
+				"validator" : function (instance, schema, self, report, parent, parentSchema, name) {
+					var format, formatValidators;
+					if (instance.getType() === "string") {
+						format = schema.getAttribute("format");
+						formatValidators = self.getValueOfProperty("formatValidators");
+						if (typeof format === "string" && formatValidators[format] !== O[format] && typeof formatValidators[format] === "function" && !formatValidators[format].call(this, instance, report)) {
+							report.addError(instance, schema, "format", "String is not in the required format", format);
+						}
+					}
+				},
+
+				// JOSHFIRE: added a couple of format validators for "uri" and "email"
+				"formatValidators" : {
+					"uri": function (instance, report) {
+						// Regular expression from @diegoperini taken from:
+						// http://mathiasbynens.be/demo/url-regex
+						// ... and only slightly adjusted for use in JavaScript
+						var reUri = /^(?:(?:https?):\/\/)(?:\S+(?::\S*)?@)?(?:(?!10(?:\.\d{1,3}){3})(?!127(?:\.\d{1,3}){3})(?!169\.254(?:\.\d{1,3}){2})(?!192\.168(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]+\-?)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]+\-?)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,})))(?::\d{2,5})?(?:\/[^\s]*)?$/i;
+						return !!reUri.test(instance._value);
+					},
+					"email": function (instance, report) {
+						// Regular expression taken from:
+						// http://www.regular-expressions.info/email.html
+						var reEmail = /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i;
+						return !!reEmail.test(instance._value);
+					}
+				}
+				// JOSHFIRE: end of custom code
+				// "formatValidators": {}
+			},
+
+			"contentEncoding" : {
+				"type" : "string",
+				"optional" : true
+			},
+
+			"default" : {
+				"type" : "any",
+				"optional" : true
+			},
+
+			"maxDecimal" : {
+				"type" : "integer",
+				"optional" : true,
+				"minimum" : 0,
+
+				"parser" : function (instance, self) {
+					if (instance.getType() === "number") {
+						return instance.getValue();
+					}
+				},
+
+				"validator" : function (instance, schema, self, report, parent, parentSchema, name) {
+					var maxDecimal, decimals;
+					if (instance.getType() === "number") {
+						maxDecimal = schema.getAttribute("maxDecimal");
+						if (typeof maxDecimal === "number") {
+							decimals = instance.getValue().toString(10).split('.')[1];
+							if (decimals && decimals.length > maxDecimal) {
+								report.addError(instance, schema, "maxDecimal", "The number of decimal places is greater then the allowed maximum", maxDecimal);
+							}
+						}
+					}
+				}
+			},
+
+			"disallow" : {
+				"type" : ["string", "array"],
+				"items" : {"type" : "string"},
+				"optional" : true,
+				"uniqueItems" : true,
+
+				"parser" : function (instance, self) {
+					if (instance.getType() === "string" || instance.getType() === "array") {
+						return instance.getValue();
+					}
+				},
+
+				"validator" : function (instance, schema, self, report, parent, parentSchema, name) {
+					var disallowedTypes = JSV.toArray(schema.getAttribute("disallow")),
+						x, xl, key, typeValidators, subreport;
+
+					//for instances that are required to be a certain type
+					if (instance.getType() !== "undefined" && disallowedTypes && disallowedTypes.length) {
+						typeValidators = self.getValueOfProperty("typeValidators") || {};
+
+						//ensure that type matches for at least one of the required types
+						for (x = 0, xl = disallowedTypes.length; x < xl; ++x) {
+							key = disallowedTypes[x];
+							if (JSV.isJSONSchema(key)) {  //this is supported draft-03 and on
+								subreport = JSV.createObject(report);
+								subreport.errors = [];
+								subreport.validated = JSV.clone(report.validated);
+								if (key.validate(instance, subreport, parent, parentSchema, name).errors.length === 0) {
+									//instance matches this schema
+									report.addError(instance, schema, "disallow", "Instance is a disallowed type", disallowedTypes);
+									return false;  
+								}
+							} else if (typeValidators[key] !== O[key] && typeof typeValidators[key] === "function") {
+								if (typeValidators[key](instance, report)) {
+									report.addError(instance, schema, "disallow", "Instance is a disallowed type", disallowedTypes);
+									return false;
+								}
+							} 
+							/*
+							else {
+								report.addError(instance, schema, "disallow", "Instance may be a disallowed type", disallowedTypes);
+								return false;
+							}
+							*/
+						}
+
+						//if we get to this point, type is valid
+						return true;
+					}
+					//else, everything is allowed if no disallowed types are specified
+					return true;
+				},
+
+				"typeValidators" : TYPE_VALIDATORS
+			},
+
+			"extends" : {
+				"type" : [{"$ref" : "#"}, "array"],
+				"items" : {"$ref" : "#"},
+				"optional" : true,
+				"default" : {},
+
+				"parser" : function (instance, self) {
+					if (instance.getType() === "object") {
+						return instance.getEnvironment().createSchema(instance, self.getEnvironment().findSchema(self.resolveURI("#")));
+					} else if (instance.getType() === "array") {
+						return JSV.mapArray(instance.getProperties(), function (instance) {
+							return instance.getEnvironment().createSchema(instance, self.getEnvironment().findSchema(self.resolveURI("#")));
+						});
+					}
+				},
+
+				"validator" : function (instance, schema, self, report, parent, parentSchema, name) {
+					var extensions = schema.getAttribute("extends"), x, xl;
+					if (extensions) {
+						if (JSV.isJSONSchema(extensions)) {
+							extensions.validate(instance, report, parent, parentSchema, name);
+						} else if (JSV.typeOf(extensions) === "array") {
+							for (x = 0, xl = extensions.length; x < xl; ++x) {
+								extensions[x].validate(instance, report, parent, parentSchema, name);
+							}
+						}
+					}
+				}
+			}
+		},
+
+		"optional" : true,
+		"default" : {},
+		"fragmentResolution" : "dot-delimited",
+
+		"parser" : function (instance, self) {
+			if (instance.getType() === "object") {
+				return instance.getEnvironment().createSchema(instance, self);
+			}
+		},
+
+		"validator" : function (instance, schema, self, report, parent, parentSchema, name) {
+			var propNames = schema.getPropertyNames(), 
+				x, xl,
+				attributeSchemas = self.getAttribute("properties"),
+				strict = instance.getEnvironment().getOption("strict"),
+				validator;
+
+			for (x in attributeSchemas) {
+				if (attributeSchemas[x] !== O[x]) {
+					if (attributeSchemas[x].getValueOfProperty("validationRequired")) {
+						JSV.pushUnique(propNames, x);
+					}
+					if (strict && attributeSchemas[x].getValueOfProperty("deprecated")) {
+						JSV.popFirst(propNames, x);
+					}
+				}
+			}
+
+			for (x = 0, xl = propNames.length; x < xl; ++x) {
+				if (attributeSchemas[propNames[x]] !== O[propNames[x]]) {
+					validator = attributeSchemas[propNames[x]].getValueOfProperty("validator");
+					if (typeof validator === "function") {
+						validator(instance, schema, attributeSchemas[propNames[x]], report, parent, parentSchema, name);
+					}
+				}
+			}
+		}
+	};
+
+	HYPERSCHEMA_00_JSON = {
+		"$schema" : "http://json-schema.org/draft-00/hyper-schema#",
+		"id" : "http://json-schema.org/draft-00/hyper-schema#",
+
+		"properties" : {
+			"links" : {
+				"type" : "array",
+				"items" : {"$ref" : "links#"},
+				"optional" : true,
+
+				"parser" : function (instance, self, arg) {
+					var links,
+						linkSchemaURI = self.getValueOfProperty("items")["$ref"],
+						linkSchema = self.getEnvironment().findSchema(linkSchemaURI),
+						linkParser = linkSchema && linkSchema.getValueOfProperty("parser"),
+						selfReferenceVariable;
+					arg = JSV.toArray(arg);
+
+					if (typeof linkParser === "function") {
+						links = JSV.mapArray(instance.getProperties(), function (link) {
+							return linkParser(link, linkSchema);
+						});
+					} else {
+						links = JSV.toArray(instance.getValue());
+					}
+
+					if (arg[0]) {
+						links = JSV.filterArray(links, function (link) {
+							return link["rel"] === arg[0];
+						});
+					}
+
+					if (arg[1]) {
+						selfReferenceVariable = self.getValueOfProperty("selfReferenceVariable");
+						links = JSV.mapArray(links, function (link) {
+							var instance = arg[1],
+								href = link["href"];
+							href = href.replace(/\{(.+)\}/g, function (str, p1, offset, s) {
+								var value; 
+								if (p1 === selfReferenceVariable) {
+									value = instance.getValue();
+								} else {
+									value = instance.getValueOfProperty(p1);
+								}
+								return value !== undefined ? String(value) : "";
+							});
+							return href ? JSV.formatURI(instance.resolveURI(href)) : href;
+						});
+					}
+
+					return links;
+				},
+
+				"selfReferenceVariable" : "-this"
+			},
+
+			"fragmentResolution" : {
+				"type" : "string",
+				"optional" : true,
+				"default" : "dot-delimited"
+			},
+
+			"root" : {
+				"type" : "boolean",
+				"optional" : true,
+				"default" : false
+			},
+
+			"readonly" : {
+				"type" : "boolean",
+				"optional" : true,
+				"default" : false
+			},
+
+			"pathStart" : {
+				"type" : "string",
+				"optional" : true,
+				"format" : "uri",
+
+				"validator" : function (instance, schema, self, report, parent, parentSchema, name) {
+					var pathStart;
+					if (instance.getType() !== "undefined") {
+						pathStart = schema.getAttribute("pathStart");
+						if (typeof pathStart === "string") {
+							//TODO: Find out what pathStart is relative to
+							if (instance.getURI().indexOf(pathStart) !== 0) {
+								report.addError(instance, schema, "pathStart", "Instance's URI does not start with " + pathStart, pathStart);
+							}
+						}
+					}
+				}
+			},
+
+			"mediaType" : {
+				"type" : "string",
+				"optional" : true,
+				"format" : "media-type"
+			},
+
+			"alternate" : {
+				"type" : "array",
+				"items" : {"$ref" : "#"},
+				"optional" : true
+			}
+		},
+
+		"links" : [
+			{
+				"href" : "{$ref}",
+				"rel" : "full"
+			},
+
+			{
+				"href" : "{$schema}",
+				"rel" : "describedby"
+			},
+
+			{
+				"href" : "{id}",
+				"rel" : "self"
+			}
+		],
+
+		"initializer" : function (instance) {
+			var link, extension, extended;
+
+			//if there is a link to a different schema, update instance
+			link = instance._schema.getLink("describedby", instance);
+			if (link && instance._schema._uri !== link) {
+				if (instance._env._schemas[link]) {
+					instance._schema = instance._env._schemas[link];
+					initializer = instance._schema.getValueOfProperty("initializer");
+					if (typeof initializer === "function") {
+						return initializer(instance);  //this function will finish initialization
+					} else {
+						return instance;  //no further initialization
+					}
+				} else if (instance._env._options["validateReferences"]) {
+					throw new InitializationError(instance, instance._schema, "{link:describedby}", "Unknown schema reference", link);
+				}
+			}
+
+			//if there is a link to the full representation, replace instance
+			link = instance._schema.getLink("full", instance);
+			if (link && instance._uri !== link) {
+				if (instance._env._schemas[link]) {
+					instance = instance._env._schemas[link];
+					return instance;  //retrieved schemas are guaranteed to be initialized
+				} else if (instance._env._options["validateReferences"]) {
+					throw new InitializationError(instance, instance._schema, "{link:full}", "Unknown schema reference", link);
+				}
+			}
+
+			//extend schema
+			extension = instance.getAttribute("extends");
+			if (JSV.isJSONSchema(extension)) {
+				extended = JSV.inherits(extension, instance, true);
+				instance = instance._env.createSchema(extended, instance._schema, instance._uri);
+			}
+
+			//if instance has a URI link to itself, update it's own URI
+			link = instance._schema.getLink("self", instance);
+			if (JSV.typeOf(link) === "string") {
+				instance._uri = JSV.formatURI(link);
+			}
+
+			return instance;
+		}
+
+		//not needed as JSV.inherits does the job for us
+		//"extends" : {"$ref" : "http://json-schema.org/schema#"}
+	};
+
+	LINKS_00_JSON = {
+		"$schema" : "http://json-schema.org/draft-00/hyper-schema#",
+		"id" : "http://json-schema.org/draft-00/links#",
+		"type" : "object",
+
+		"properties" : {
+			"href" : {
+				"type" : "string"
+			},
+
+			"rel" : {
+				"type" : "string"
+			},
+
+			"method" : {
+				"type" : "string",
+				"default" : "GET",
+				"optional" : true
+			},
+
+			"enctype" : {
+				"type" : "string",
+				"requires" : "method",
+				"optional" : true
+			},
+
+			"properties" : {
+				"type" : "object",
+				"additionalProperties" : {"$ref" : "hyper-schema#"},
+				"optional" : true,
+
+				"parser" : function (instance, self, arg) {
+					var env = instance.getEnvironment(),
+						selfEnv = self.getEnvironment(),
+						additionalPropertiesSchemaURI = self.getValueOfProperty("additionalProperties")["$ref"];
+					if (instance.getType() === "object") {
+						if (arg) {
+							return env.createSchema(instance.getProperty(arg), selfEnv.findSchema(self.resolveURI(additionalPropertiesSchemaURI)));
+						} else {
+							return JSV.mapObject(instance.getProperties(), function (instance) {
+								return env.createSchema(instance, selfEnv.findSchema(self.resolveURI(additionalPropertiesSchemaURI)));
+							});
+						}
+					}
+				}
+			}
+		},
+
+		"parser" : function (instance, self) {
+			var selfProperties = self.getProperty("properties");
+			if (instance.getType() === "object") {
+				return JSV.mapObject(instance.getProperties(), function (property, key) {
+					var propertySchema = selfProperties.getProperty(key),
+						parser = propertySchema && propertySchema.getValueOfProperty("parser");
+					if (typeof parser === "function") {
+						return parser(property, propertySchema);
+					}
+					//else
+					return property.getValue();
+				});
+			}
+			return instance.getValue();
+		}
+	};
+
+	ENVIRONMENT.setOption("defaultFragmentDelimiter", ".");
+	ENVIRONMENT.setOption("defaultSchemaURI", "http://json-schema.org/draft-00/schema#");  //updated later
+
+	SCHEMA_00 = ENVIRONMENT.createSchema(SCHEMA_00_JSON, true, "http://json-schema.org/draft-00/schema#");
+	HYPERSCHEMA_00 = ENVIRONMENT.createSchema(JSV.inherits(SCHEMA_00, ENVIRONMENT.createSchema(HYPERSCHEMA_00_JSON, true, "http://json-schema.org/draft-00/hyper-schema#"), true), true, "http://json-schema.org/draft-00/hyper-schema#");
+
+	ENVIRONMENT.setOption("defaultSchemaURI", "http://json-schema.org/draft-00/hyper-schema#");
+
+	LINKS_00 = ENVIRONMENT.createSchema(LINKS_00_JSON, HYPERSCHEMA_00, "http://json-schema.org/draft-00/links#");
+
+	//We need to reinitialize these 3 schemas as they all reference each other
+	SCHEMA_00 = ENVIRONMENT.createSchema(SCHEMA_00.getValue(), HYPERSCHEMA_00, "http://json-schema.org/draft-00/schema#");
+	HYPERSCHEMA_00 = ENVIRONMENT.createSchema(HYPERSCHEMA_00.getValue(), HYPERSCHEMA_00, "http://json-schema.org/draft-00/hyper-schema#");
+	LINKS_00 = ENVIRONMENT.createSchema(LINKS_00.getValue(), HYPERSCHEMA_00, "http://json-schema.org/draft-00/links#");
+
+	//
+	// draft-01
+	//
+
+	SCHEMA_01_JSON = JSV.inherits(SCHEMA_00_JSON, {
+		"$schema" : "http://json-schema.org/draft-01/hyper-schema#",
+		"id" : "http://json-schema.org/draft-01/schema#"
+	});
+
+	HYPERSCHEMA_01_JSON = JSV.inherits(HYPERSCHEMA_00_JSON, {
+		"$schema" : "http://json-schema.org/draft-01/hyper-schema#",
+		"id" : "http://json-schema.org/draft-01/hyper-schema#"
+	});
+
+	LINKS_01_JSON = JSV.inherits(LINKS_00_JSON, {
+		"$schema" : "http://json-schema.org/draft-01/hyper-schema#",
+		"id" : "http://json-schema.org/draft-01/links#"
+	});
+
+	ENVIRONMENT.setOption("defaultSchemaURI", "http://json-schema.org/draft-01/schema#");  //update later
+
+	SCHEMA_01 = ENVIRONMENT.createSchema(SCHEMA_01_JSON, true, "http://json-schema.org/draft-01/schema#");
+	HYPERSCHEMA_01 = ENVIRONMENT.createSchema(JSV.inherits(SCHEMA_01, ENVIRONMENT.createSchema(HYPERSCHEMA_01_JSON, true, "http://json-schema.org/draft-01/hyper-schema#"), true), true, "http://json-schema.org/draft-01/hyper-schema#");
+
+	ENVIRONMENT.setOption("defaultSchemaURI", "http://json-schema.org/draft-01/hyper-schema#");
+
+	LINKS_01 = ENVIRONMENT.createSchema(LINKS_01_JSON, HYPERSCHEMA_01, "http://json-schema.org/draft-01/links#");
+
+	//We need to reinitialize these 3 schemas as they all reference each other
+	SCHEMA_01 = ENVIRONMENT.createSchema(SCHEMA_01.getValue(), HYPERSCHEMA_01, "http://json-schema.org/draft-01/schema#");
+	HYPERSCHEMA_01 = ENVIRONMENT.createSchema(HYPERSCHEMA_01.getValue(), HYPERSCHEMA_01, "http://json-schema.org/draft-01/hyper-schema#");
+	LINKS_01 = ENVIRONMENT.createSchema(LINKS_01.getValue(), HYPERSCHEMA_01, "http://json-schema.org/draft-01/links#");
+
+	//
+	// draft-02
+	//
+
+	SCHEMA_02_JSON = JSV.inherits(SCHEMA_01_JSON, {
+		"$schema" : "http://json-schema.org/draft-02/hyper-schema#",
+		"id" : "http://json-schema.org/draft-02/schema#",
+
+		"properties" : {
+			"uniqueItems" : {
+				"type" : "boolean",
+				"optional" : true,
+				"default" : false,
+
+				"parser" : function (instance, self) {
+					return !!instance.getValue();
+				},
+
+				"validator" : function (instance, schema, self, report, parent, parentSchema, name) {
+					var value, x, xl, y, yl;
+					if (instance.getType() === "array" && schema.getAttribute("uniqueItems")) {
+						value = instance.getProperties();
+						for (x = 0, xl = value.length - 1; x < xl; ++x) {
+							for (y = x + 1, yl = value.length; y < yl; ++y) {
+								if (value[x].equals(value[y])) {
+									report.addError(instance, schema, "uniqueItems", "Array can only contain unique items", { x : x, y : y });
+								}
+							}
+						}
+					}
+				}
+			},
+
+			"maxDecimal" : {
+				"deprecated" : true
+			},
+
+			"divisibleBy" : {
+				"type" : "number",
+				"minimum" : 0,
+				"minimumCanEqual" : false,
+				"optional" : true,
+
+				"parser" : function (instance, self) {
+					if (instance.getType() === "number") {
+						return instance.getValue();
+					}
+				},
+
+				"validator" : function (instance, schema, self, report, parent, parentSchema, name) {
+					var divisor;
+					if (instance.getType() === "number") {
+						divisor = schema.getAttribute("divisibleBy");
+						if (divisor === 0) {
+							report.addError(instance, schema, "divisibleBy", "Nothing is divisible by 0", divisor);
+						} else if (divisor !== 1 && ((instance.getValue() / divisor) % 1) !== 0) {
+							report.addError(instance, schema, "divisibleBy", "Number is not divisible by " + divisor, divisor);
+						}
+					}
+				}
+			}
+		},
+
+		"fragmentResolution" : "slash-delimited"
+	});
+
+	HYPERSCHEMA_02_JSON = JSV.inherits(HYPERSCHEMA_01_JSON, {
+		"id" : "http://json-schema.org/draft-02/hyper-schema#",
+
+		"properties" : {
+			"fragmentResolution" : {
+				"default" : "slash-delimited"
+			}
+		}
+	});
+
+	LINKS_02_JSON = JSV.inherits(LINKS_01_JSON, {
+		"$schema" : "http://json-schema.org/draft-02/hyper-schema#",
+		"id" : "http://json-schema.org/draft-02/links#",
+
+		"properties" : {
+			"targetSchema" : {
+				"$ref" : "hyper-schema#",
+
+				//need this here because parsers are run before links are resolved
+				"parser" : HYPERSCHEMA_01.getAttribute("parser")
+			}
+		}
+	});
+
+	ENVIRONMENT.setOption("defaultFragmentDelimiter", "/");
+	ENVIRONMENT.setOption("defaultSchemaURI", "http://json-schema.org/draft-02/schema#");  //update later
+
+	SCHEMA_02 = ENVIRONMENT.createSchema(SCHEMA_02_JSON, true, "http://json-schema.org/draft-02/schema#");
+	HYPERSCHEMA_02 = ENVIRONMENT.createSchema(JSV.inherits(SCHEMA_02, ENVIRONMENT.createSchema(HYPERSCHEMA_02_JSON, true, "http://json-schema.org/draft-02/hyper-schema#"), true), true, "http://json-schema.org/draft-02/hyper-schema#");
+
+	ENVIRONMENT.setOption("defaultSchemaURI", "http://json-schema.org/draft-02/hyper-schema#");
+
+	LINKS_02 = ENVIRONMENT.createSchema(LINKS_02_JSON, HYPERSCHEMA_02, "http://json-schema.org/draft-02/links#");
+
+	//We need to reinitialize these 3 schemas as they all reference each other
+	SCHEMA_02 = ENVIRONMENT.createSchema(SCHEMA_02.getValue(), HYPERSCHEMA_02, "http://json-schema.org/draft-02/schema#");
+	HYPERSCHEMA_02 = ENVIRONMENT.createSchema(HYPERSCHEMA_02.getValue(), HYPERSCHEMA_02, "http://json-schema.org/draft-02/hyper-schema#");
+	LINKS_02 = ENVIRONMENT.createSchema(LINKS_02.getValue(), HYPERSCHEMA_02, "http://json-schema.org/draft-02/links#");
+
+	//
+	// draft-03
+	//
+
+	function getMatchedPatternProperties(instance, schema, report, self) {
+		var matchedProperties = {}, patternProperties, pattern, regexp, properties, key;
+
+		if (instance.getType() === "object") {
+			patternProperties = schema.getAttribute("patternProperties");
+			properties = instance.getProperties();
+			for (pattern in patternProperties) {
+				if (patternProperties[pattern] !== O[pattern]) {
+					regexp = null;
+					try {
+						regexp = new RegExp(pattern);
+					} catch (e) {
+						if (report) {
+							report.addError(schema, self, "patternProperties", "Invalid pattern", pattern);
+						}
+					}
+
+					if (regexp) {
+						for (key in properties) {
+							if (properties[key] !== O[key]  && regexp.test(key)) {
+								matchedProperties[key] = matchedProperties[key] ? JSV.pushUnique(matchedProperties[key], patternProperties[pattern]) : [ patternProperties[pattern] ];
+							}
+						}
+					}
+				}
+			}
+		}
+
+		return matchedProperties;
+	}
+
+	SCHEMA_03_JSON = JSV.inherits(SCHEMA_02_JSON, {
+		"$schema" : "http://json-schema.org/draft-03/schema#",
+		"id" : "http://json-schema.org/draft-03/schema#",
+
+		"properties" : {
+			"patternProperties" : {
+				"type" : "object",
+				"additionalProperties" : {"$ref" : "#"},
+				"default" : {},
+
+				"parser" : SCHEMA_02.getValueOfProperty("properties")["properties"]["parser"],
+
+				"validator" : function (instance, schema, self, report, parent, parentSchema, name) {
+					var matchedProperties, key, x;
+					if (instance.getType() === "object") {
+						matchedProperties = getMatchedPatternProperties(instance, schema, report, self);
+						for (key in matchedProperties) {
+							if (matchedProperties[key] !== O[key]) {
+								x = matchedProperties[key].length;
+								while (x--) {
+									matchedProperties[key][x].validate(instance.getProperty(key), report, instance, schema, key);
+								}
+							}
+						}
+					}
+				}
+			},
+
+			"additionalProperties" : {
+				"validator" : function (instance, schema, self, report, parent, parentSchema, name) {
+					var additionalProperties, propertySchemas, properties, matchedProperties, key;
+					if (instance.getType() === "object") {
+						additionalProperties = schema.getAttribute("additionalProperties");
+						propertySchemas = schema.getAttribute("properties") || {};
+						properties = instance.getProperties();
+						matchedProperties = getMatchedPatternProperties(instance, schema);
+						for (key in properties) {
+							if (properties[key] !== O[key] && properties[key] && propertySchemas[key] === O[key] && matchedProperties[key] === O[key]) {
+								if (JSV.isJSONSchema(additionalProperties)) {
+									additionalProperties.validate(properties[key], report, instance, schema, key);
+								} else if (additionalProperties === false) {
+									report.addError(instance, schema, "additionalProperties", "Additional properties are not allowed", additionalProperties);
+								}
+							}
+						}
+					}
+				}
+			},
+
+			"items" : {
+				"validator" : function (instance, schema, self, report, parent, parentSchema, name) {
+					var properties, items, x, xl, itemSchema, additionalItems;
+
+					if (instance.getType() === "array") {
+						properties = instance.getProperties();
+						items = schema.getAttribute("items");
+						additionalItems = schema.getAttribute("additionalItems");
+
+						if (JSV.typeOf(items) === "array") {
+							for (x = 0, xl = properties.length; x < xl; ++x) {
+								itemSchema = items[x] || additionalItems;
+								if (itemSchema !== false) {
+									itemSchema.validate(properties[x], report, instance, schema, x);
+								} else {
+									report.addError(instance, schema, "additionalItems", "Additional items are not allowed", itemSchema);
+								}
+							}
+						} else {
+							itemSchema = items || additionalItems;
+							for (x = 0, xl = properties.length; x < xl; ++x) {
+								itemSchema.validate(properties[x], report, instance, schema, x);
+							}
+						}
+					}
+				}
+			},
+
+			"additionalItems" : {
+				"type" : [{"$ref" : "#"}, "boolean"],
+				"default" : {},
+
+				"parser" : SCHEMA_02.getValueOfProperty("properties")["additionalProperties"]["parser"],
+
+				"validator" : function (instance, schema, self, report, parent, parentSchema, name) {
+					var additionalItems, properties, x, xl;
+					//only validate if the "items" attribute is undefined
+					if (instance.getType() === "array" && schema.getProperty("items").getType() === "undefined") {
+						additionalItems = schema.getAttribute("additionalItems");
+						properties = instance.getProperties();
+
+						if (additionalItems !== false) {
+							for (x = 0, xl = properties.length; x < xl; ++x) {
+								additionalItems.validate(properties[x], report, instance, schema, x);
+							}
+						} else if (properties.length) {
+							report.addError(instance, schema, "additionalItems", "Additional items are not allowed", additionalItems);
+						}
+					}
+				}
+			},
+
+			"optional" : {
+				"validationRequired" : false,
+				"deprecated" : true
+			},
+
+			"required" : {
+				"type" : "boolean",
+				"default" : false,
+
+				"parser" : function (instance, self) {
+					return !!instance.getValue();
+				},
+
+				"validator" : function (instance, schema, self, report, parent, parentSchema, name) {
+					if (instance.getType() === "undefined" && schema.getAttribute("required")) {
+						report.addError(instance, schema, "required", "Property is required", true);
+					}
+				}
+			},
+
+			"requires" : {
+				"deprecated" : true
+			},
+
+			"dependencies" : {
+				"type" : "object",
+				"additionalProperties" : {
+					"type" : ["string", "array", {"$ref" : "#"}],
+					"items" : {
+						"type" : "string"
+					}
+				},
+				"default" : {},
+
+				"parser" : function (instance, self, arg) {
+					function parseProperty(property) {
+						var type = property.getType();
+						if (type === "string" || type === "array") {
+							return property.getValue();
+						} else if (type === "object") {
+							return property.getEnvironment().createSchema(property, self.getEnvironment().findSchema(self.resolveURI("#")));
+						}
+					}
+
+					if (instance.getType() === "object") {
+						if (arg) {
+							return parseProperty(instance.getProperty(arg));
+						} else {
+							return JSV.mapObject(instance.getProperties(), parseProperty);
+						}
+					}
+					//else
+					return {};
+				},
+
+				"validator" : function (instance, schema, self, report, parent, parentSchema, name) {
+					var dependencies, key, dependency, type, x, xl;
+					if (instance.getType() === "object") {
+						dependencies = schema.getAttribute("dependencies");
+						for (key in dependencies) {
+							if (dependencies[key] !== O[key] && instance.getProperty(key).getType() !== "undefined") {
+								dependency = dependencies[key];
+								type = JSV.typeOf(dependency);
+								if (type === "string") {
+									if (instance.getProperty(dependency).getType() === "undefined") {
+										report.addError(instance, schema, "dependencies", 'Property "' + key + '" requires sibling property "' + dependency + '"', dependencies);
+									}
+								} else if (type === "array") {
+									for (x = 0, xl = dependency.length; x < xl; ++x) {
+										if (instance.getProperty(dependency[x]).getType() === "undefined") {
+											report.addError(instance, schema, "dependencies", 'Property "' + key + '" requires sibling property "' + dependency[x] + '"', dependencies);
+										}
+									}
+								} else if (JSV.isJSONSchema(dependency)) {
+									dependency.validate(instance, report);
+								}
+							}
+						}
+					}
+				}
+			},
+
+			"minimumCanEqual" : {
+				"deprecated" : true
+			},
+
+			"maximumCanEqual" : {
+				"deprecated" : true
+			},
+
+			"exclusiveMinimum" : {
+				"type" : "boolean",
+				"default" : false,
+
+				"parser" : function (instance, self) {
+					return !!instance.getValue();
+				}
+			},
+
+			"exclusiveMaximum" : {
+				"type" : "boolean",
+				"default" : false,
+
+				"parser" : function (instance, self) {
+					return !!instance.getValue();
+				}
+			},
+
+			"minimum" : {
+				"validator" : function (instance, schema, self, report, parent, parentSchema, name) {
+					var minimum, exclusiveMinimum;
+					if (instance.getType() === "number") {
+						minimum = schema.getAttribute("minimum");
+						exclusiveMinimum = schema.getAttribute("exclusiveMinimum") || (!instance.getEnvironment().getOption("strict") && !schema.getAttribute("minimumCanEqual"));
+						if (typeof minimum === "number" && (instance.getValue() < minimum || (exclusiveMinimum === true && instance.getValue() === minimum))) {
+							report.addError(instance, schema, "minimum", "Number is less then the required minimum value", minimum);
+						}
+					}
+				}
+			},
+
+			"maximum" : {
+				"validator" : function (instance, schema, self, report, parent, parentSchema, name) {
+					var maximum, exclusiveMaximum;
+					if (instance.getType() === "number") {
+						maximum = schema.getAttribute("maximum");
+						exclusiveMaximum = schema.getAttribute("exclusiveMaximum") || (!instance.getEnvironment().getOption("strict") && !schema.getAttribute("maximumCanEqual"));
+						if (typeof maximum === "number" && (instance.getValue() > maximum || (exclusiveMaximum === true && instance.getValue() === maximum))) {
+							report.addError(instance, schema, "maximum", "Number is greater then the required maximum value", maximum);
+						}
+					}
+				}
+			},
+
+			"contentEncoding" : {
+				"deprecated" : true
+			},
+
+			"divisibleBy" : {
+				"exclusiveMinimum" : true
+			},
+
+			"disallow" : {
+				"items" : {
+					"type" : ["string", {"$ref" : "#"}]
+				},
+
+				"parser" : SCHEMA_02_JSON["properties"]["type"]["parser"]
+			},
+
+			"id" : {
+				"type" : "string",
+				"format" : "uri"
+			},
+
+			"$ref" : {
+				"type" : "string",
+				"format" : "uri"
+			},
+
+			"$schema" : {
+				"type" : "string",
+				"format" : "uri"
+			}
+		},
+
+		"dependencies" : {
+			"exclusiveMinimum" : "minimum",
+			"exclusiveMaximum" : "maximum"
+		},
+
+		"initializer" : function (instance) {
+			var link, extension, extended,
+				schemaLink = instance.getValueOfProperty("$schema"),
+				refLink = instance.getValueOfProperty("$ref"),
+				idLink = instance.getValueOfProperty("id");
+
+			//if there is a link to a different schema, update instance
+			if (schemaLink) {
+				link = instance.resolveURI(schemaLink);
+				if (link && instance._schema._uri !== link) {
+					if (instance._env._schemas[link]) {
+						instance._schema = instance._env._schemas[link];
+						initializer = instance._schema.getValueOfProperty("initializer");
+						if (typeof initializer === "function") {
+							return initializer(instance);  //this function will finish initialization
+						} else {
+							return instance;  //no further initialization
+						}
+					} else if (instance._env._options["validateReferences"]) {
+						throw new InitializationError(instance, instance._schema, "$schema", "Unknown schema reference", link);
+					}
+				}
+			}
+
+			//if there is a link to the full representation, replace instance
+			if (refLink) {
+				link = instance.resolveURI(refLink);
+				if (link && instance._uri !== link) {
+					if (instance._env._schemas[link]) {
+						instance = instance._env._schemas[link];
+						return instance;  //retrieved schemas are guaranteed to be initialized
+					} else if (instance._env._options["validateReferences"]) {
+						throw new InitializationError(instance, instance._schema, "$ref", "Unknown schema reference", link);
+					}
+				}
+			}
+
+			//extend schema
+			extension = instance.getAttribute("extends");
+			if (JSV.isJSONSchema(extension)) {
+				extended = JSV.inherits(extension, instance, true);
+				instance = instance._env.createSchema(extended, instance._schema, instance._uri);
+			}
+
+			//if instance has a URI link to itself, update it's own URI
+			if (idLink) {
+				link = instance.resolveURI(idLink);
+				if (JSV.typeOf(link) === "string") {
+					instance._uri = JSV.formatURI(link);
+				}
+			}
+
+			return instance;
+		}
+	});
+
+	HYPERSCHEMA_03_JSON = JSV.inherits(HYPERSCHEMA_02_JSON, {
+		"$schema" : "http://json-schema.org/draft-03/hyper-schema#",
+		"id" : "http://json-schema.org/draft-03/hyper-schema#",
+
+		"properties" : {
+			"links" : {
+				"selfReferenceVariable" : "@"
+			},
+
+			"root" : {
+				"deprecated" : true
+			},
+
+			"contentEncoding" : {
+				"deprecated" : false  //moved from core to hyper
+			},
+
+			"alternate" : {
+				"deprecated" : true
+			}
+		}
+	});
+
+	LINKS_03_JSON = JSV.inherits(LINKS_02_JSON, {
+		"$schema" : "http://json-schema.org/draft-03/hyper-schema#",
+		"id" : "http://json-schema.org/draft-03/links#",
+
+		"properties" : {
+			"href" : {
+				"required" : true,
+				"format" : "link-description-object-template"
+			},
+
+			"rel" : {
+				"required" : true
+			},
+
+			"properties" : {
+				"deprecated" : true
+			},
+
+			"schema" : {"$ref" : "http://json-schema.org/draft-03/hyper-schema#"}
+		}
+	});
+
+	ENVIRONMENT.setOption("validateReferences", true);
+	ENVIRONMENT.setOption("defaultSchemaURI", "http://json-schema.org/draft-03/schema#");  //update later
+
+	//prevent reference errors
+	ENVIRONMENT.createSchema({}, true, "http://json-schema.org/draft-03/schema#");
+	ENVIRONMENT.createSchema({}, true, "http://json-schema.org/draft-03/hyper-schema#");
+	ENVIRONMENT.createSchema({}, true, "http://json-schema.org/draft-03/links#");
+
+	SCHEMA_03 = ENVIRONMENT.createSchema(SCHEMA_03_JSON, true, "http://json-schema.org/draft-03/schema#");
+	HYPERSCHEMA_03 = ENVIRONMENT.createSchema(JSV.inherits(SCHEMA_03, ENVIRONMENT.createSchema(HYPERSCHEMA_03_JSON, true, "http://json-schema.org/draft-03/hyper-schema#"), true), true, "http://json-schema.org/draft-03/hyper-schema#");
+	LINKS_03 = ENVIRONMENT.createSchema(LINKS_03_JSON, true, "http://json-schema.org/draft-03/links#");
+
+	ENVIRONMENT.setOption("defaultSchemaURI", "http://json-schema.org/draft-03/hyper-schema#");
+
+	//We need to reinitialize these schemas as they reference each other
+	HYPERSCHEMA_03 = ENVIRONMENT.createSchema(HYPERSCHEMA_03.getValue(), HYPERSCHEMA_03, "http://json-schema.org/draft-03/hyper-schema#");
+
+	ENVIRONMENT.setOption("latestJSONSchemaSchemaURI", "http://json-schema.org/draft-03/schema#");
+	ENVIRONMENT.setOption("latestJSONSchemaHyperSchemaURI", "http://json-schema.org/draft-03/hyper-schema#");
+	ENVIRONMENT.setOption("latestJSONSchemaLinksURI", "http://json-schema.org/draft-03/links#");
+
+	//
+	//Latest JSON Schema
+	//
+
+	//Hack, but WAY faster then instantiating a new schema
+	ENVIRONMENT._schemas["http://json-schema.org/schema#"] = SCHEMA_03;
+	ENVIRONMENT._schemas["http://json-schema.org/hyper-schema#"] = HYPERSCHEMA_03;
+	ENVIRONMENT._schemas["http://json-schema.org/links#"] = LINKS_03;
+
+	//
+	//register environment
+	//
+
+	JSV.registerEnvironment("json-schema-draft-03", ENVIRONMENT);
+	if (!JSV.getDefaultEnvironmentID() || JSV.getDefaultEnvironmentID() === "json-schema-draft-01" || JSV.getDefaultEnvironmentID() === "json-schema-draft-02") {
+		JSV.setDefaultEnvironmentID("json-schema-draft-03");
+	}
+
+}());
+
+
+global.JSONFormValidator = exports.JSV;
+})(this, false);

+ 1548 - 0
src/http/deps/underscore.js

@@ -0,0 +1,1548 @@
+//     Underscore.js 1.8.3
+//     http://underscorejs.org
+//     (c) 2009-2015 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
+//     Underscore may be freely distributed under the MIT license.
+
+(function() {
+
+  // Baseline setup
+  // --------------
+
+  // Establish the root object, `window` in the browser, or `exports` on the server.
+  var root = this;
+
+  // Save the previous value of the `_` variable.
+  var previousUnderscore = root._;
+
+  // Save bytes in the minified (but not gzipped) version:
+  var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype;
+
+  // Create quick reference variables for speed access to core prototypes.
+  var
+    push             = ArrayProto.push,
+    slice            = ArrayProto.slice,
+    toString         = ObjProto.toString,
+    hasOwnProperty   = ObjProto.hasOwnProperty;
+
+  // All **ECMAScript 5** native function implementations that we hope to use
+  // are declared here.
+  var
+    nativeIsArray      = Array.isArray,
+    nativeKeys         = Object.keys,
+    nativeBind         = FuncProto.bind,
+    nativeCreate       = Object.create;
+
+  // Naked function reference for surrogate-prototype-swapping.
+  var Ctor = function(){};
+
+  // Create a safe reference to the Underscore object for use below.
+  var _ = function(obj) {
+    if (obj instanceof _) return obj;
+    if (!(this instanceof _)) return new _(obj);
+    this._wrapped = obj;
+  };
+
+  // Export the Underscore object for **Node.js**, with
+  // backwards-compatibility for the old `require()` API. If we're in
+  // the browser, add `_` as a global object.
+  if (typeof exports !== 'undefined') {
+    if (typeof module !== 'undefined' && module.exports) {
+      exports = module.exports = _;
+    }
+    exports._ = _;
+  } else {
+    root._ = _;
+  }
+
+  // Current version.
+  _.VERSION = '1.8.3';
+
+  // Internal function that returns an efficient (for current engines) version
+  // of the passed-in callback, to be repeatedly applied in other Underscore
+  // functions.
+  var optimizeCb = function(func, context, argCount) {
+    if (context === void 0) return func;
+    switch (argCount == null ? 3 : argCount) {
+      case 1: return function(value) {
+        return func.call(context, value);
+      };
+      case 2: return function(value, other) {
+        return func.call(context, value, other);
+      };
+      case 3: return function(value, index, collection) {
+        return func.call(context, value, index, collection);
+      };
+      case 4: return function(accumulator, value, index, collection) {
+        return func.call(context, accumulator, value, index, collection);
+      };
+    }
+    return function() {
+      return func.apply(context, arguments);
+    };
+  };
+
+  // A mostly-internal function to generate callbacks that can be applied
+  // to each element in a collection, returning the desired result — either
+  // identity, an arbitrary callback, a property matcher, or a property accessor.
+  var cb = function(value, context, argCount) {
+    if (value == null) return _.identity;
+    if (_.isFunction(value)) return optimizeCb(value, context, argCount);
+    if (_.isObject(value)) return _.matcher(value);
+    return _.property(value);
+  };
+  _.iteratee = function(value, context) {
+    return cb(value, context, Infinity);
+  };
+
+  // An internal function for creating assigner functions.
+  var createAssigner = function(keysFunc, undefinedOnly) {
+    return function(obj) {
+      var length = arguments.length;
+      if (length < 2 || obj == null) return obj;
+      for (var index = 1; index < length; index++) {
+        var source = arguments[index],
+            keys = keysFunc(source),
+            l = keys.length;
+        for (var i = 0; i < l; i++) {
+          var key = keys[i];
+          if (!undefinedOnly || obj[key] === void 0) obj[key] = source[key];
+        }
+      }
+      return obj;
+    };
+  };
+
+  // An internal function for creating a new object that inherits from another.
+  var baseCreate = function(prototype) {
+    if (!_.isObject(prototype)) return {};
+    if (nativeCreate) return nativeCreate(prototype);
+    Ctor.prototype = prototype;
+    var result = new Ctor;
+    Ctor.prototype = null;
+    return result;
+  };
+
+  var property = function(key) {
+    return function(obj) {
+      return obj == null ? void 0 : obj[key];
+    };
+  };
+
+  // Helper for collection methods to determine whether a collection
+  // should be iterated as an array or as an object
+  // Related: http://people.mozilla.org/~jorendorff/es6-draft.html#sec-tolength
+  // Avoids a very nasty iOS 8 JIT bug on ARM-64. #2094
+  var MAX_ARRAY_INDEX = Math.pow(2, 53) - 1;
+  var getLength = property('length');
+  var isArrayLike = function(collection) {
+    var length = getLength(collection);
+    return typeof length == 'number' && length >= 0 && length <= MAX_ARRAY_INDEX;
+  };
+
+  // Collection Functions
+  // --------------------
+
+  // The cornerstone, an `each` implementation, aka `forEach`.
+  // Handles raw objects in addition to array-likes. Treats all
+  // sparse array-likes as if they were dense.
+  _.each = _.forEach = function(obj, iteratee, context) {
+    iteratee = optimizeCb(iteratee, context);
+    var i, length;
+    if (isArrayLike(obj)) {
+      for (i = 0, length = obj.length; i < length; i++) {
+        iteratee(obj[i], i, obj);
+      }
+    } else {
+      var keys = _.keys(obj);
+      for (i = 0, length = keys.length; i < length; i++) {
+        iteratee(obj[keys[i]], keys[i], obj);
+      }
+    }
+    return obj;
+  };
+
+  // Return the results of applying the iteratee to each element.
+  _.map = _.collect = function(obj, iteratee, context) {
+    iteratee = cb(iteratee, context);
+    var keys = !isArrayLike(obj) && _.keys(obj),
+        length = (keys || obj).length,
+        results = Array(length);
+    for (var index = 0; index < length; index++) {
+      var currentKey = keys ? keys[index] : index;
+      results[index] = iteratee(obj[currentKey], currentKey, obj);
+    }
+    return results;
+  };
+
+  // Create a reducing function iterating left or right.
+  function createReduce(dir) {
+    // Optimized iterator function as using arguments.length
+    // in the main function will deoptimize the, see #1991.
+    function iterator(obj, iteratee, memo, keys, index, length) {
+      for (; index >= 0 && index < length; index += dir) {
+        var currentKey = keys ? keys[index] : index;
+        memo = iteratee(memo, obj[currentKey], currentKey, obj);
+      }
+      return memo;
+    }
+
+    return function(obj, iteratee, memo, context) {
+      iteratee = optimizeCb(iteratee, context, 4);
+      var keys = !isArrayLike(obj) && _.keys(obj),
+          length = (keys || obj).length,
+          index = dir > 0 ? 0 : length - 1;
+      // Determine the initial value if none is provided.
+      if (arguments.length < 3) {
+        memo = obj[keys ? keys[index] : index];
+        index += dir;
+      }
+      return iterator(obj, iteratee, memo, keys, index, length);
+    };
+  }
+
+  // **Reduce** builds up a single result from a list of values, aka `inject`,
+  // or `foldl`.
+  _.reduce = _.foldl = _.inject = createReduce(1);
+
+  // The right-associative version of reduce, also known as `foldr`.
+  _.reduceRight = _.foldr = createReduce(-1);
+
+  // Return the first value which passes a truth test. Aliased as `detect`.
+  _.find = _.detect = function(obj, predicate, context) {
+    var key;
+    if (isArrayLike(obj)) {
+      key = _.findIndex(obj, predicate, context);
+    } else {
+      key = _.findKey(obj, predicate, context);
+    }
+    if (key !== void 0 && key !== -1) return obj[key];
+  };
+
+  // Return all the elements that pass a truth test.
+  // Aliased as `select`.
+  _.filter = _.select = function(obj, predicate, context) {
+    var results = [];
+    predicate = cb(predicate, context);
+    _.each(obj, function(value, index, list) {
+      if (predicate(value, index, list)) results.push(value);
+    });
+    return results;
+  };
+
+  // Return all the elements for which a truth test fails.
+  _.reject = function(obj, predicate, context) {
+    return _.filter(obj, _.negate(cb(predicate)), context);
+  };
+
+  // Determine whether all of the elements match a truth test.
+  // Aliased as `all`.
+  _.every = _.all = function(obj, predicate, context) {
+    predicate = cb(predicate, context);
+    var keys = !isArrayLike(obj) && _.keys(obj),
+        length = (keys || obj).length;
+    for (var index = 0; index < length; index++) {
+      var currentKey = keys ? keys[index] : index;
+      if (!predicate(obj[currentKey], currentKey, obj)) return false;
+    }
+    return true;
+  };
+
+  // Determine if at least one element in the object matches a truth test.
+  // Aliased as `any`.
+  _.some = _.any = function(obj, predicate, context) {
+    predicate = cb(predicate, context);
+    var keys = !isArrayLike(obj) && _.keys(obj),
+        length = (keys || obj).length;
+    for (var index = 0; index < length; index++) {
+      var currentKey = keys ? keys[index] : index;
+      if (predicate(obj[currentKey], currentKey, obj)) return true;
+    }
+    return false;
+  };
+
+  // Determine if the array or object contains a given item (using `===`).
+  // Aliased as `includes` and `include`.
+  _.contains = _.includes = _.include = function(obj, item, fromIndex, guard) {
+    if (!isArrayLike(obj)) obj = _.values(obj);
+    if (typeof fromIndex != 'number' || guard) fromIndex = 0;
+    return _.indexOf(obj, item, fromIndex) >= 0;
+  };
+
+  // Invoke a method (with arguments) on every item in a collection.
+  _.invoke = function(obj, method) {
+    var args = slice.call(arguments, 2);
+    var isFunc = _.isFunction(method);
+    return _.map(obj, function(value) {
+      var func = isFunc ? method : value[method];
+      return func == null ? func : func.apply(value, args);
+    });
+  };
+
+  // Convenience version of a common use case of `map`: fetching a property.
+  _.pluck = function(obj, key) {
+    return _.map(obj, _.property(key));
+  };
+
+  // Convenience version of a common use case of `filter`: selecting only objects
+  // containing specific `key:value` pairs.
+  _.where = function(obj, attrs) {
+    return _.filter(obj, _.matcher(attrs));
+  };
+
+  // Convenience version of a common use case of `find`: getting the first object
+  // containing specific `key:value` pairs.
+  _.findWhere = function(obj, attrs) {
+    return _.find(obj, _.matcher(attrs));
+  };
+
+  // Return the maximum element (or element-based computation).
+  _.max = function(obj, iteratee, context) {
+    var result = -Infinity, lastComputed = -Infinity,
+        value, computed;
+    if (iteratee == null && obj != null) {
+      obj = isArrayLike(obj) ? obj : _.values(obj);
+      for (var i = 0, length = obj.length; i < length; i++) {
+        value = obj[i];
+        if (value > result) {
+          result = value;
+        }
+      }
+    } else {
+      iteratee = cb(iteratee, context);
+      _.each(obj, function(value, index, list) {
+        computed = iteratee(value, index, list);
+        if (computed > lastComputed || computed === -Infinity && result === -Infinity) {
+          result = value;
+          lastComputed = computed;
+        }
+      });
+    }
+    return result;
+  };
+
+  // Return the minimum element (or element-based computation).
+  _.min = function(obj, iteratee, context) {
+    var result = Infinity, lastComputed = Infinity,
+        value, computed;
+    if (iteratee == null && obj != null) {
+      obj = isArrayLike(obj) ? obj : _.values(obj);
+      for (var i = 0, length = obj.length; i < length; i++) {
+        value = obj[i];
+        if (value < result) {
+          result = value;
+        }
+      }
+    } else {
+      iteratee = cb(iteratee, context);
+      _.each(obj, function(value, index, list) {
+        computed = iteratee(value, index, list);
+        if (computed < lastComputed || computed === Infinity && result === Infinity) {
+          result = value;
+          lastComputed = computed;
+        }
+      });
+    }
+    return result;
+  };
+
+  // Shuffle a collection, using the modern version of the
+  // [Fisher-Yates shuffle](http://en.wikipedia.org/wiki/Fisher–Yates_shuffle).
+  _.shuffle = function(obj) {
+    var set = isArrayLike(obj) ? obj : _.values(obj);
+    var length = set.length;
+    var shuffled = Array(length);
+    for (var index = 0, rand; index < length; index++) {
+      rand = _.random(0, index);
+      if (rand !== index) shuffled[index] = shuffled[rand];
+      shuffled[rand] = set[index];
+    }
+    return shuffled;
+  };
+
+  // Sample **n** random values from a collection.
+  // If **n** is not specified, returns a single random element.
+  // The internal `guard` argument allows it to work with `map`.
+  _.sample = function(obj, n, guard) {
+    if (n == null || guard) {
+      if (!isArrayLike(obj)) obj = _.values(obj);
+      return obj[_.random(obj.length - 1)];
+    }
+    return _.shuffle(obj).slice(0, Math.max(0, n));
+  };
+
+  // Sort the object's values by a criterion produced by an iteratee.
+  _.sortBy = function(obj, iteratee, context) {
+    iteratee = cb(iteratee, context);
+    return _.pluck(_.map(obj, function(value, index, list) {
+      return {
+        value: value,
+        index: index,
+        criteria: iteratee(value, index, list)
+      };
+    }).sort(function(left, right) {
+      var a = left.criteria;
+      var b = right.criteria;
+      if (a !== b) {
+        if (a > b || a === void 0) return 1;
+        if (a < b || b === void 0) return -1;
+      }
+      return left.index - right.index;
+    }), 'value');
+  };
+
+  // An internal function used for aggregate "group by" operations.
+  var group = function(behavior) {
+    return function(obj, iteratee, context) {
+      var result = {};
+      iteratee = cb(iteratee, context);
+      _.each(obj, function(value, index) {
+        var key = iteratee(value, index, obj);
+        behavior(result, value, key);
+      });
+      return result;
+    };
+  };
+
+  // Groups the object's values by a criterion. Pass either a string attribute
+  // to group by, or a function that returns the criterion.
+  _.groupBy = group(function(result, value, key) {
+    if (_.has(result, key)) result[key].push(value); else result[key] = [value];
+  });
+
+  // Indexes the object's values by a criterion, similar to `groupBy`, but for
+  // when you know that your index values will be unique.
+  _.indexBy = group(function(result, value, key) {
+    result[key] = value;
+  });
+
+  // Counts instances of an object that group by a certain criterion. Pass
+  // either a string attribute to count by, or a function that returns the
+  // criterion.
+  _.countBy = group(function(result, value, key) {
+    if (_.has(result, key)) result[key]++; else result[key] = 1;
+  });
+
+  // Safely create a real, live array from anything iterable.
+  _.toArray = function(obj) {
+    if (!obj) return [];
+    if (_.isArray(obj)) return slice.call(obj);
+    if (isArrayLike(obj)) return _.map(obj, _.identity);
+    return _.values(obj);
+  };
+
+  // Return the number of elements in an object.
+  _.size = function(obj) {
+    if (obj == null) return 0;
+    return isArrayLike(obj) ? obj.length : _.keys(obj).length;
+  };
+
+  // Split a collection into two arrays: one whose elements all satisfy the given
+  // predicate, and one whose elements all do not satisfy the predicate.
+  _.partition = function(obj, predicate, context) {
+    predicate = cb(predicate, context);
+    var pass = [], fail = [];
+    _.each(obj, function(value, key, obj) {
+      (predicate(value, key, obj) ? pass : fail).push(value);
+    });
+    return [pass, fail];
+  };
+
+  // Array Functions
+  // ---------------
+
+  // Get the first element of an array. Passing **n** will return the first N
+  // values in the array. Aliased as `head` and `take`. The **guard** check
+  // allows it to work with `_.map`.
+  _.first = _.head = _.take = function(array, n, guard) {
+    if (array == null) return void 0;
+    if (n == null || guard) return array[0];
+    return _.initial(array, array.length - n);
+  };
+
+  // Returns everything but the last entry of the array. Especially useful on
+  // the arguments object. Passing **n** will return all the values in
+  // the array, excluding the last N.
+  _.initial = function(array, n, guard) {
+    return slice.call(array, 0, Math.max(0, array.length - (n == null || guard ? 1 : n)));
+  };
+
+  // Get the last element of an array. Passing **n** will return the last N
+  // values in the array.
+  _.last = function(array, n, guard) {
+    if (array == null) return void 0;
+    if (n == null || guard) return array[array.length - 1];
+    return _.rest(array, Math.max(0, array.length - n));
+  };
+
+  // Returns everything but the first entry of the array. Aliased as `tail` and `drop`.
+  // Especially useful on the arguments object. Passing an **n** will return
+  // the rest N values in the array.
+  _.rest = _.tail = _.drop = function(array, n, guard) {
+    return slice.call(array, n == null || guard ? 1 : n);
+  };
+
+  // Trim out all falsy values from an array.
+  _.compact = function(array) {
+    return _.filter(array, _.identity);
+  };
+
+  // Internal implementation of a recursive `flatten` function.
+  var flatten = function(input, shallow, strict, startIndex) {
+    var output = [], idx = 0;
+    for (var i = startIndex || 0, length = getLength(input); i < length; i++) {
+      var value = input[i];
+      if (isArrayLike(value) && (_.isArray(value) || _.isArguments(value))) {
+        //flatten current level of array or arguments object
+        if (!shallow) value = flatten(value, shallow, strict);
+        var j = 0, len = value.length;
+        output.length += len;
+        while (j < len) {
+          output[idx++] = value[j++];
+        }
+      } else if (!strict) {
+        output[idx++] = value;
+      }
+    }
+    return output;
+  };
+
+  // Flatten out an array, either recursively (by default), or just one level.
+  _.flatten = function(array, shallow) {
+    return flatten(array, shallow, false);
+  };
+
+  // Return a version of the array that does not contain the specified value(s).
+  _.without = function(array) {
+    return _.difference(array, slice.call(arguments, 1));
+  };
+
+  // Produce a duplicate-free version of the array. If the array has already
+  // been sorted, you have the option of using a faster algorithm.
+  // Aliased as `unique`.
+  _.uniq = _.unique = function(array, isSorted, iteratee, context) {
+    if (!_.isBoolean(isSorted)) {
+      context = iteratee;
+      iteratee = isSorted;
+      isSorted = false;
+    }
+    if (iteratee != null) iteratee = cb(iteratee, context);
+    var result = [];
+    var seen = [];
+    for (var i = 0, length = getLength(array); i < length; i++) {
+      var value = array[i],
+          computed = iteratee ? iteratee(value, i, array) : value;
+      if (isSorted) {
+        if (!i || seen !== computed) result.push(value);
+        seen = computed;
+      } else if (iteratee) {
+        if (!_.contains(seen, computed)) {
+          seen.push(computed);
+          result.push(value);
+        }
+      } else if (!_.contains(result, value)) {
+        result.push(value);
+      }
+    }
+    return result;
+  };
+
+  // Produce an array that contains the union: each distinct element from all of
+  // the passed-in arrays.
+  _.union = function() {
+    return _.uniq(flatten(arguments, true, true));
+  };
+
+  // Produce an array that contains every item shared between all the
+  // passed-in arrays.
+  _.intersection = function(array) {
+    var result = [];
+    var argsLength = arguments.length;
+    for (var i = 0, length = getLength(array); i < length; i++) {
+      var item = array[i];
+      if (_.contains(result, item)) continue;
+      for (var j = 1; j < argsLength; j++) {
+        if (!_.contains(arguments[j], item)) break;
+      }
+      if (j === argsLength) result.push(item);
+    }
+    return result;
+  };
+
+  // Take the difference between one array and a number of other arrays.
+  // Only the elements present in just the first array will remain.
+  _.difference = function(array) {
+    var rest = flatten(arguments, true, true, 1);
+    return _.filter(array, function(value){
+      return !_.contains(rest, value);
+    });
+  };
+
+  // Zip together multiple lists into a single array -- elements that share
+  // an index go together.
+  _.zip = function() {
+    return _.unzip(arguments);
+  };
+
+  // Complement of _.zip. Unzip accepts an array of arrays and groups
+  // each array's elements on shared indices
+  _.unzip = function(array) {
+    var length = array && _.max(array, getLength).length || 0;
+    var result = Array(length);
+
+    for (var index = 0; index < length; index++) {
+      result[index] = _.pluck(array, index);
+    }
+    return result;
+  };
+
+  // Converts lists into objects. Pass either a single array of `[key, value]`
+  // pairs, or two parallel arrays of the same length -- one of keys, and one of
+  // the corresponding values.
+  _.object = function(list, values) {
+    var result = {};
+    for (var i = 0, length = getLength(list); i < length; i++) {
+      if (values) {
+        result[list[i]] = values[i];
+      } else {
+        result[list[i][0]] = list[i][1];
+      }
+    }
+    return result;
+  };
+
+  // Generator function to create the findIndex and findLastIndex functions
+  function createPredicateIndexFinder(dir) {
+    return function(array, predicate, context) {
+      predicate = cb(predicate, context);
+      var length = getLength(array);
+      var index = dir > 0 ? 0 : length - 1;
+      for (; index >= 0 && index < length; index += dir) {
+        if (predicate(array[index], index, array)) return index;
+      }
+      return -1;
+    };
+  }
+
+  // Returns the first index on an array-like that passes a predicate test
+  _.findIndex = createPredicateIndexFinder(1);
+  _.findLastIndex = createPredicateIndexFinder(-1);
+
+  // Use a comparator function to figure out the smallest index at which
+  // an object should be inserted so as to maintain order. Uses binary search.
+  _.sortedIndex = function(array, obj, iteratee, context) {
+    iteratee = cb(iteratee, context, 1);
+    var value = iteratee(obj);
+    var low = 0, high = getLength(array);
+    while (low < high) {
+      var mid = Math.floor((low + high) / 2);
+      if (iteratee(array[mid]) < value) low = mid + 1; else high = mid;
+    }
+    return low;
+  };
+
+  // Generator function to create the indexOf and lastIndexOf functions
+  function createIndexFinder(dir, predicateFind, sortedIndex) {
+    return function(array, item, idx) {
+      var i = 0, length = getLength(array);
+      if (typeof idx == 'number') {
+        if (dir > 0) {
+            i = idx >= 0 ? idx : Math.max(idx + length, i);
+        } else {
+            length = idx >= 0 ? Math.min(idx + 1, length) : idx + length + 1;
+        }
+      } else if (sortedIndex && idx && length) {
+        idx = sortedIndex(array, item);
+        return array[idx] === item ? idx : -1;
+      }
+      if (item !== item) {
+        idx = predicateFind(slice.call(array, i, length), _.isNaN);
+        return idx >= 0 ? idx + i : -1;
+      }
+      for (idx = dir > 0 ? i : length - 1; idx >= 0 && idx < length; idx += dir) {
+        if (array[idx] === item) return idx;
+      }
+      return -1;
+    };
+  }
+
+  // Return the position of the first occurrence of an item in an array,
+  // or -1 if the item is not included in the array.
+  // If the array is large and already in sort order, pass `true`
+  // for **isSorted** to use binary search.
+  _.indexOf = createIndexFinder(1, _.findIndex, _.sortedIndex);
+  _.lastIndexOf = createIndexFinder(-1, _.findLastIndex);
+
+  // Generate an integer Array containing an arithmetic progression. A port of
+  // the native Python `range()` function. See
+  // [the Python documentation](http://docs.python.org/library/functions.html#range).
+  _.range = function(start, stop, step) {
+    if (stop == null) {
+      stop = start || 0;
+      start = 0;
+    }
+    step = step || 1;
+
+    var length = Math.max(Math.ceil((stop - start) / step), 0);
+    var range = Array(length);
+
+    for (var idx = 0; idx < length; idx++, start += step) {
+      range[idx] = start;
+    }
+
+    return range;
+  };
+
+  // Function (ahem) Functions
+  // ------------------
+
+  // Determines whether to execute a function as a constructor
+  // or a normal function with the provided arguments
+  var executeBound = function(sourceFunc, boundFunc, context, callingContext, args) {
+    if (!(callingContext instanceof boundFunc)) return sourceFunc.apply(context, args);
+    var self = baseCreate(sourceFunc.prototype);
+    var result = sourceFunc.apply(self, args);
+    if (_.isObject(result)) return result;
+    return self;
+  };
+
+  // Create a function bound to a given object (assigning `this`, and arguments,
+  // optionally). Delegates to **ECMAScript 5**'s native `Function.bind` if
+  // available.
+  _.bind = function(func, context) {
+    if (nativeBind && func.bind === nativeBind) return nativeBind.apply(func, slice.call(arguments, 1));
+    if (!_.isFunction(func)) throw new TypeError('Bind must be called on a function');
+    var args = slice.call(arguments, 2);
+    var bound = function() {
+      return executeBound(func, bound, context, this, args.concat(slice.call(arguments)));
+    };
+    return bound;
+  };
+
+  // Partially apply a function by creating a version that has had some of its
+  // arguments pre-filled, without changing its dynamic `this` context. _ acts
+  // as a placeholder, allowing any combination of arguments to be pre-filled.
+  _.partial = function(func) {
+    var boundArgs = slice.call(arguments, 1);
+    var bound = function() {
+      var position = 0, length = boundArgs.length;
+      var args = Array(length);
+      for (var i = 0; i < length; i++) {
+        args[i] = boundArgs[i] === _ ? arguments[position++] : boundArgs[i];
+      }
+      while (position < arguments.length) args.push(arguments[position++]);
+      return executeBound(func, bound, this, this, args);
+    };
+    return bound;
+  };
+
+  // Bind a number of an object's methods to that object. Remaining arguments
+  // are the method names to be bound. Useful for ensuring that all callbacks
+  // defined on an object belong to it.
+  _.bindAll = function(obj) {
+    var i, length = arguments.length, key;
+    if (length <= 1) throw new Error('bindAll must be passed function names');
+    for (i = 1; i < length; i++) {
+      key = arguments[i];
+      obj[key] = _.bind(obj[key], obj);
+    }
+    return obj;
+  };
+
+  // Memoize an expensive function by storing its results.
+  _.memoize = function(func, hasher) {
+    var memoize = function(key) {
+      var cache = memoize.cache;
+      var address = '' + (hasher ? hasher.apply(this, arguments) : key);
+      if (!_.has(cache, address)) cache[address] = func.apply(this, arguments);
+      return cache[address];
+    };
+    memoize.cache = {};
+    return memoize;
+  };
+
+  // Delays a function for the given number of milliseconds, and then calls
+  // it with the arguments supplied.
+  _.delay = function(func, wait) {
+    var args = slice.call(arguments, 2);
+    return setTimeout(function(){
+      return func.apply(null, args);
+    }, wait);
+  };
+
+  // Defers a function, scheduling it to run after the current call stack has
+  // cleared.
+  _.defer = _.partial(_.delay, _, 1);
+
+  // Returns a function, that, when invoked, will only be triggered at most once
+  // during a given window of time. Normally, the throttled function will run
+  // as much as it can, without ever going more than once per `wait` duration;
+  // but if you'd like to disable the execution on the leading edge, pass
+  // `{leading: false}`. To disable execution on the trailing edge, ditto.
+  _.throttle = function(func, wait, options) {
+    var context, args, result;
+    var timeout = null;
+    var previous = 0;
+    if (!options) options = {};
+    var later = function() {
+      previous = options.leading === false ? 0 : _.now();
+      timeout = null;
+      result = func.apply(context, args);
+      if (!timeout) context = args = null;
+    };
+    return function() {
+      var now = _.now();
+      if (!previous && options.leading === false) previous = now;
+      var remaining = wait - (now - previous);
+      context = this;
+      args = arguments;
+      if (remaining <= 0 || remaining > wait) {
+        if (timeout) {
+          clearTimeout(timeout);
+          timeout = null;
+        }
+        previous = now;
+        result = func.apply(context, args);
+        if (!timeout) context = args = null;
+      } else if (!timeout && options.trailing !== false) {
+        timeout = setTimeout(later, remaining);
+      }
+      return result;
+    };
+  };
+
+  // Returns a function, that, as long as it continues to be invoked, will not
+  // be triggered. The function will be called after it stops being called for
+  // N milliseconds. If `immediate` is passed, trigger the function on the
+  // leading edge, instead of the trailing.
+  _.debounce = function(func, wait, immediate) {
+    var timeout, args, context, timestamp, result;
+
+    var later = function() {
+      var last = _.now() - timestamp;
+
+      if (last < wait && last >= 0) {
+        timeout = setTimeout(later, wait - last);
+      } else {
+        timeout = null;
+        if (!immediate) {
+          result = func.apply(context, args);
+          if (!timeout) context = args = null;
+        }
+      }
+    };
+
+    return function() {
+      context = this;
+      args = arguments;
+      timestamp = _.now();
+      var callNow = immediate && !timeout;
+      if (!timeout) timeout = setTimeout(later, wait);
+      if (callNow) {
+        result = func.apply(context, args);
+        context = args = null;
+      }
+
+      return result;
+    };
+  };
+
+  // Returns the first function passed as an argument to the second,
+  // allowing you to adjust arguments, run code before and after, and
+  // conditionally execute the original function.
+  _.wrap = function(func, wrapper) {
+    return _.partial(wrapper, func);
+  };
+
+  // Returns a negated version of the passed-in predicate.
+  _.negate = function(predicate) {
+    return function() {
+      return !predicate.apply(this, arguments);
+    };
+  };
+
+  // Returns a function that is the composition of a list of functions, each
+  // consuming the return value of the function that follows.
+  _.compose = function() {
+    var args = arguments;
+    var start = args.length - 1;
+    return function() {
+      var i = start;
+      var result = args[start].apply(this, arguments);
+      while (i--) result = args[i].call(this, result);
+      return result;
+    };
+  };
+
+  // Returns a function that will only be executed on and after the Nth call.
+  _.after = function(times, func) {
+    return function() {
+      if (--times < 1) {
+        return func.apply(this, arguments);
+      }
+    };
+  };
+
+  // Returns a function that will only be executed up to (but not including) the Nth call.
+  _.before = function(times, func) {
+    var memo;
+    return function() {
+      if (--times > 0) {
+        memo = func.apply(this, arguments);
+      }
+      if (times <= 1) func = null;
+      return memo;
+    };
+  };
+
+  // Returns a function that will be executed at most one time, no matter how
+  // often you call it. Useful for lazy initialization.
+  _.once = _.partial(_.before, 2);
+
+  // Object Functions
+  // ----------------
+
+  // Keys in IE < 9 that won't be iterated by `for key in ...` and thus missed.
+  var hasEnumBug = !{toString: null}.propertyIsEnumerable('toString');
+  var nonEnumerableProps = ['valueOf', 'isPrototypeOf', 'toString',
+                      'propertyIsEnumerable', 'hasOwnProperty', 'toLocaleString'];
+
+  function collectNonEnumProps(obj, keys) {
+    var nonEnumIdx = nonEnumerableProps.length;
+    var constructor = obj.constructor;
+    var proto = (_.isFunction(constructor) && constructor.prototype) || ObjProto;
+
+    // Constructor is a special case.
+    var prop = 'constructor';
+    if (_.has(obj, prop) && !_.contains(keys, prop)) keys.push(prop);
+
+    while (nonEnumIdx--) {
+      prop = nonEnumerableProps[nonEnumIdx];
+      if (prop in obj && obj[prop] !== proto[prop] && !_.contains(keys, prop)) {
+        keys.push(prop);
+      }
+    }
+  }
+
+  // Retrieve the names of an object's own properties.
+  // Delegates to **ECMAScript 5**'s native `Object.keys`
+  _.keys = function(obj) {
+    if (!_.isObject(obj)) return [];
+    if (nativeKeys) return nativeKeys(obj);
+    var keys = [];
+    for (var key in obj) if (_.has(obj, key)) keys.push(key);
+    // Ahem, IE < 9.
+    if (hasEnumBug) collectNonEnumProps(obj, keys);
+    return keys;
+  };
+
+  // Retrieve all the property names of an object.
+  _.allKeys = function(obj) {
+    if (!_.isObject(obj)) return [];
+    var keys = [];
+    for (var key in obj) keys.push(key);
+    // Ahem, IE < 9.
+    if (hasEnumBug) collectNonEnumProps(obj, keys);
+    return keys;
+  };
+
+  // Retrieve the values of an object's properties.
+  _.values = function(obj) {
+    var keys = _.keys(obj);
+    var length = keys.length;
+    var values = Array(length);
+    for (var i = 0; i < length; i++) {
+      values[i] = obj[keys[i]];
+    }
+    return values;
+  };
+
+  // Returns the results of applying the iteratee to each element of the object
+  // In contrast to _.map it returns an object
+  _.mapObject = function(obj, iteratee, context) {
+    iteratee = cb(iteratee, context);
+    var keys =  _.keys(obj),
+          length = keys.length,
+          results = {},
+          currentKey;
+      for (var index = 0; index < length; index++) {
+        currentKey = keys[index];
+        results[currentKey] = iteratee(obj[currentKey], currentKey, obj);
+      }
+      return results;
+  };
+
+  // Convert an object into a list of `[key, value]` pairs.
+  _.pairs = function(obj) {
+    var keys = _.keys(obj);
+    var length = keys.length;
+    var pairs = Array(length);
+    for (var i = 0; i < length; i++) {
+      pairs[i] = [keys[i], obj[keys[i]]];
+    }
+    return pairs;
+  };
+
+  // Invert the keys and values of an object. The values must be serializable.
+  _.invert = function(obj) {
+    var result = {};
+    var keys = _.keys(obj);
+    for (var i = 0, length = keys.length; i < length; i++) {
+      result[obj[keys[i]]] = keys[i];
+    }
+    return result;
+  };
+
+  // Return a sorted list of the function names available on the object.
+  // Aliased as `methods`
+  _.functions = _.methods = function(obj) {
+    var names = [];
+    for (var key in obj) {
+      if (_.isFunction(obj[key])) names.push(key);
+    }
+    return names.sort();
+  };
+
+  // Extend a given object with all the properties in passed-in object(s).
+  _.extend = createAssigner(_.allKeys);
+
+  // Assigns a given object with all the own properties in the passed-in object(s)
+  // (https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object/assign)
+  _.extendOwn = _.assign = createAssigner(_.keys);
+
+  // Returns the first key on an object that passes a predicate test
+  _.findKey = function(obj, predicate, context) {
+    predicate = cb(predicate, context);
+    var keys = _.keys(obj), key;
+    for (var i = 0, length = keys.length; i < length; i++) {
+      key = keys[i];
+      if (predicate(obj[key], key, obj)) return key;
+    }
+  };
+
+  // Return a copy of the object only containing the whitelisted properties.
+  _.pick = function(object, oiteratee, context) {
+    var result = {}, obj = object, iteratee, keys;
+    if (obj == null) return result;
+    if (_.isFunction(oiteratee)) {
+      keys = _.allKeys(obj);
+      iteratee = optimizeCb(oiteratee, context);
+    } else {
+      keys = flatten(arguments, false, false, 1);
+      iteratee = function(value, key, obj) { return key in obj; };
+      obj = Object(obj);
+    }
+    for (var i = 0, length = keys.length; i < length; i++) {
+      var key = keys[i];
+      var value = obj[key];
+      if (iteratee(value, key, obj)) result[key] = value;
+    }
+    return result;
+  };
+
+   // Return a copy of the object without the blacklisted properties.
+  _.omit = function(obj, iteratee, context) {
+    if (_.isFunction(iteratee)) {
+      iteratee = _.negate(iteratee);
+    } else {
+      var keys = _.map(flatten(arguments, false, false, 1), String);
+      iteratee = function(value, key) {
+        return !_.contains(keys, key);
+      };
+    }
+    return _.pick(obj, iteratee, context);
+  };
+
+  // Fill in a given object with default properties.
+  _.defaults = createAssigner(_.allKeys, true);
+
+  // Creates an object that inherits from the given prototype object.
+  // If additional properties are provided then they will be added to the
+  // created object.
+  _.create = function(prototype, props) {
+    var result = baseCreate(prototype);
+    if (props) _.extendOwn(result, props);
+    return result;
+  };
+
+  // Create a (shallow-cloned) duplicate of an object.
+  _.clone = function(obj) {
+    if (!_.isObject(obj)) return obj;
+    return _.isArray(obj) ? obj.slice() : _.extend({}, obj);
+  };
+
+  // Invokes interceptor with the obj, and then returns obj.
+  // The primary purpose of this method is to "tap into" a method chain, in
+  // order to perform operations on intermediate results within the chain.
+  _.tap = function(obj, interceptor) {
+    interceptor(obj);
+    return obj;
+  };
+
+  // Returns whether an object has a given set of `key:value` pairs.
+  _.isMatch = function(object, attrs) {
+    var keys = _.keys(attrs), length = keys.length;
+    if (object == null) return !length;
+    var obj = Object(object);
+    for (var i = 0; i < length; i++) {
+      var key = keys[i];
+      if (attrs[key] !== obj[key] || !(key in obj)) return false;
+    }
+    return true;
+  };
+
+
+  // Internal recursive comparison function for `isEqual`.
+  var eq = function(a, b, aStack, bStack) {
+    // Identical objects are equal. `0 === -0`, but they aren't identical.
+    // See the [Harmony `egal` proposal](http://wiki.ecmascript.org/doku.php?id=harmony:egal).
+    if (a === b) return a !== 0 || 1 / a === 1 / b;
+    // A strict comparison is necessary because `null == undefined`.
+    if (a == null || b == null) return a === b;
+    // Unwrap any wrapped objects.
+    if (a instanceof _) a = a._wrapped;
+    if (b instanceof _) b = b._wrapped;
+    // Compare `[[Class]]` names.
+    var className = toString.call(a);
+    if (className !== toString.call(b)) return false;
+    switch (className) {
+      // Strings, numbers, regular expressions, dates, and booleans are compared by value.
+      case '[object RegExp]':
+      // RegExps are coerced to strings for comparison (Note: '' + /a/i === '/a/i')
+      case '[object String]':
+        // Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is
+        // equivalent to `new String("5")`.
+        return '' + a === '' + b;
+      case '[object Number]':
+        // `NaN`s are equivalent, but non-reflexive.
+        // Object(NaN) is equivalent to NaN
+        if (+a !== +a) return +b !== +b;
+        // An `egal` comparison is performed for other numeric values.
+        return +a === 0 ? 1 / +a === 1 / b : +a === +b;
+      case '[object Date]':
+      case '[object Boolean]':
+        // Coerce dates and booleans to numeric primitive values. Dates are compared by their
+        // millisecond representations. Note that invalid dates with millisecond representations
+        // of `NaN` are not equivalent.
+        return +a === +b;
+    }
+
+    var areArrays = className === '[object Array]';
+    if (!areArrays) {
+      if (typeof a != 'object' || typeof b != 'object') return false;
+
+      // Objects with different constructors are not equivalent, but `Object`s or `Array`s
+      // from different frames are.
+      var aCtor = a.constructor, bCtor = b.constructor;
+      if (aCtor !== bCtor && !(_.isFunction(aCtor) && aCtor instanceof aCtor &&
+                               _.isFunction(bCtor) && bCtor instanceof bCtor)
+                          && ('constructor' in a && 'constructor' in b)) {
+        return false;
+      }
+    }
+    // Assume equality for cyclic structures. The algorithm for detecting cyclic
+    // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`.
+
+    // Initializing stack of traversed objects.
+    // It's done here since we only need them for objects and arrays comparison.
+    aStack = aStack || [];
+    bStack = bStack || [];
+    var length = aStack.length;
+    while (length--) {
+      // Linear search. Performance is inversely proportional to the number of
+      // unique nested structures.
+      if (aStack[length] === a) return bStack[length] === b;
+    }
+
+    // Add the first object to the stack of traversed objects.
+    aStack.push(a);
+    bStack.push(b);
+
+    // Recursively compare objects and arrays.
+    if (areArrays) {
+      // Compare array lengths to determine if a deep comparison is necessary.
+      length = a.length;
+      if (length !== b.length) return false;
+      // Deep compare the contents, ignoring non-numeric properties.
+      while (length--) {
+        if (!eq(a[length], b[length], aStack, bStack)) return false;
+      }
+    } else {
+      // Deep compare objects.
+      var keys = _.keys(a), key;
+      length = keys.length;
+      // Ensure that both objects contain the same number of properties before comparing deep equality.
+      if (_.keys(b).length !== length) return false;
+      while (length--) {
+        // Deep compare each member
+        key = keys[length];
+        if (!(_.has(b, key) && eq(a[key], b[key], aStack, bStack))) return false;
+      }
+    }
+    // Remove the first object from the stack of traversed objects.
+    aStack.pop();
+    bStack.pop();
+    return true;
+  };
+
+  // Perform a deep comparison to check if two objects are equal.
+  _.isEqual = function(a, b) {
+    return eq(a, b);
+  };
+
+  // Is a given array, string, or object empty?
+  // An "empty" object has no enumerable own-properties.
+  _.isEmpty = function(obj) {
+    if (obj == null) return true;
+    if (isArrayLike(obj) && (_.isArray(obj) || _.isString(obj) || _.isArguments(obj))) return obj.length === 0;
+    return _.keys(obj).length === 0;
+  };
+
+  // Is a given value a DOM element?
+  _.isElement = function(obj) {
+    return !!(obj && obj.nodeType === 1);
+  };
+
+  // Is a given value an array?
+  // Delegates to ECMA5's native Array.isArray
+  _.isArray = nativeIsArray || function(obj) {
+    return toString.call(obj) === '[object Array]';
+  };
+
+  // Is a given variable an object?
+  _.isObject = function(obj) {
+    var type = typeof obj;
+    return type === 'function' || type === 'object' && !!obj;
+  };
+
+  // Add some isType methods: isArguments, isFunction, isString, isNumber, isDate, isRegExp, isError.
+  _.each(['Arguments', 'Function', 'String', 'Number', 'Date', 'RegExp', 'Error'], function(name) {
+    _['is' + name] = function(obj) {
+      return toString.call(obj) === '[object ' + name + ']';
+    };
+  });
+
+  // Define a fallback version of the method in browsers (ahem, IE < 9), where
+  // there isn't any inspectable "Arguments" type.
+  if (!_.isArguments(arguments)) {
+    _.isArguments = function(obj) {
+      return _.has(obj, 'callee');
+    };
+  }
+
+  // Optimize `isFunction` if appropriate. Work around some typeof bugs in old v8,
+  // IE 11 (#1621), and in Safari 8 (#1929).
+  if (typeof /./ != 'function' && typeof Int8Array != 'object') {
+    _.isFunction = function(obj) {
+      return typeof obj == 'function' || false;
+    };
+  }
+
+  // Is a given object a finite number?
+  _.isFinite = function(obj) {
+    return isFinite(obj) && !isNaN(parseFloat(obj));
+  };
+
+  // Is the given value `NaN`? (NaN is the only number which does not equal itself).
+  _.isNaN = function(obj) {
+    return _.isNumber(obj) && obj !== +obj;
+  };
+
+  // Is a given value a boolean?
+  _.isBoolean = function(obj) {
+    return obj === true || obj === false || toString.call(obj) === '[object Boolean]';
+  };
+
+  // Is a given value equal to null?
+  _.isNull = function(obj) {
+    return obj === null;
+  };
+
+  // Is a given variable undefined?
+  _.isUndefined = function(obj) {
+    return obj === void 0;
+  };
+
+  // Shortcut function for checking if an object has a given property directly
+  // on itself (in other words, not on a prototype).
+  _.has = function(obj, key) {
+    return obj != null && hasOwnProperty.call(obj, key);
+  };
+
+  // Utility Functions
+  // -----------------
+
+  // Run Underscore.js in *noConflict* mode, returning the `_` variable to its
+  // previous owner. Returns a reference to the Underscore object.
+  _.noConflict = function() {
+    root._ = previousUnderscore;
+    return this;
+  };
+
+  // Keep the identity function around for default iteratees.
+  _.identity = function(value) {
+    return value;
+  };
+
+  // Predicate-generating functions. Often useful outside of Underscore.
+  _.constant = function(value) {
+    return function() {
+      return value;
+    };
+  };
+
+  _.noop = function(){};
+
+  _.property = property;
+
+  // Generates a function for a given object that returns a given property.
+  _.propertyOf = function(obj) {
+    return obj == null ? function(){} : function(key) {
+      return obj[key];
+    };
+  };
+
+  // Returns a predicate for checking whether an object has a given set of
+  // `key:value` pairs.
+  _.matcher = _.matches = function(attrs) {
+    attrs = _.extendOwn({}, attrs);
+    return function(obj) {
+      return _.isMatch(obj, attrs);
+    };
+  };
+
+  // Run a function **n** times.
+  _.times = function(n, iteratee, context) {
+    var accum = Array(Math.max(0, n));
+    iteratee = optimizeCb(iteratee, context, 1);
+    for (var i = 0; i < n; i++) accum[i] = iteratee(i);
+    return accum;
+  };
+
+  // Return a random integer between min and max (inclusive).
+  _.random = function(min, max) {
+    if (max == null) {
+      max = min;
+      min = 0;
+    }
+    return min + Math.floor(Math.random() * (max - min + 1));
+  };
+
+  // A (possibly faster) way to get the current timestamp as an integer.
+  _.now = Date.now || function() {
+    return new Date().getTime();
+  };
+
+   // List of HTML entities for escaping.
+  var escapeMap = {
+    '&': '&amp;',
+    '<': '&lt;',
+    '>': '&gt;',
+    '"': '&quot;',
+    "'": '&#x27;',
+    '`': '&#x60;'
+  };
+  var unescapeMap = _.invert(escapeMap);
+
+  // Functions for escaping and unescaping strings to/from HTML interpolation.
+  var createEscaper = function(map) {
+    var escaper = function(match) {
+      return map[match];
+    };
+    // Regexes for identifying a key that needs to be escaped
+    var source = '(?:' + _.keys(map).join('|') + ')';
+    var testRegexp = RegExp(source);
+    var replaceRegexp = RegExp(source, 'g');
+    return function(string) {
+      string = string == null ? '' : '' + string;
+      return testRegexp.test(string) ? string.replace(replaceRegexp, escaper) : string;
+    };
+  };
+  _.escape = createEscaper(escapeMap);
+  _.unescape = createEscaper(unescapeMap);
+
+  // If the value of the named `property` is a function then invoke it with the
+  // `object` as context; otherwise, return it.
+  _.result = function(object, property, fallback) {
+    var value = object == null ? void 0 : object[property];
+    if (value === void 0) {
+      value = fallback;
+    }
+    return _.isFunction(value) ? value.call(object) : value;
+  };
+
+  // Generate a unique integer id (unique within the entire client session).
+  // Useful for temporary DOM ids.
+  var idCounter = 0;
+  _.uniqueId = function(prefix) {
+    var id = ++idCounter + '';
+    return prefix ? prefix + id : id;
+  };
+
+  // By default, Underscore uses ERB-style template delimiters, change the
+  // following template settings to use alternative delimiters.
+  _.templateSettings = {
+    evaluate    : /<%([\s\S]+?)%>/g,
+    interpolate : /<%=([\s\S]+?)%>/g,
+    escape      : /<%-([\s\S]+?)%>/g
+  };
+
+  // When customizing `templateSettings`, if you don't want to define an
+  // interpolation, evaluation or escaping regex, we need one that is
+  // guaranteed not to match.
+  var noMatch = /(.)^/;
+
+  // Certain characters need to be escaped so that they can be put into a
+  // string literal.
+  var escapes = {
+    "'":      "'",
+    '\\':     '\\',
+    '\r':     'r',
+    '\n':     'n',
+    '\u2028': 'u2028',
+    '\u2029': 'u2029'
+  };
+
+  var escaper = /\\|'|\r|\n|\u2028|\u2029/g;
+
+  var escapeChar = function(match) {
+    return '\\' + escapes[match];
+  };
+
+  // JavaScript micro-templating, similar to John Resig's implementation.
+  // Underscore templating handles arbitrary delimiters, preserves whitespace,
+  // and correctly escapes quotes within interpolated code.
+  // NB: `oldSettings` only exists for backwards compatibility.
+  _.template = function(text, settings, oldSettings) {
+    if (!settings && oldSettings) settings = oldSettings;
+    settings = _.defaults({}, settings, _.templateSettings);
+
+    // Combine delimiters into one regular expression via alternation.
+    var matcher = RegExp([
+      (settings.escape || noMatch).source,
+      (settings.interpolate || noMatch).source,
+      (settings.evaluate || noMatch).source
+    ].join('|') + '|$', 'g');
+
+    // Compile the template source, escaping string literals appropriately.
+    var index = 0;
+    var source = "__p+='";
+    text.replace(matcher, function(match, escape, interpolate, evaluate, offset) {
+      source += text.slice(index, offset).replace(escaper, escapeChar);
+      index = offset + match.length;
+
+      if (escape) {
+        source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'";
+      } else if (interpolate) {
+        source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'";
+      } else if (evaluate) {
+        source += "';\n" + evaluate + "\n__p+='";
+      }
+
+      // Adobe VMs need the match returned to produce the correct offest.
+      return match;
+    });
+    source += "';\n";
+
+    // If a variable is not specified, place data values in local scope.
+    if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n';
+
+    source = "var __t,__p='',__j=Array.prototype.join," +
+      "print=function(){__p+=__j.call(arguments,'');};\n" +
+      source + 'return __p;\n';
+
+    try {
+      var render = new Function(settings.variable || 'obj', '_', source);
+    } catch (e) {
+      e.source = source;
+      throw e;
+    }
+
+    var template = function(data) {
+      return render.call(this, data, _);
+    };
+
+    // Provide the compiled source as a convenience for precompilation.
+    var argument = settings.variable || 'obj';
+    template.source = 'function(' + argument + '){\n' + source + '}';
+
+    return template;
+  };
+
+  // Add a "chain" function. Start chaining a wrapped Underscore object.
+  _.chain = function(obj) {
+    var instance = _(obj);
+    instance._chain = true;
+    return instance;
+  };
+
+  // OOP
+  // ---------------
+  // If Underscore is called as a function, it returns a wrapped object that
+  // can be used OO-style. This wrapper holds altered versions of all the
+  // underscore functions. Wrapped objects may be chained.
+
+  // Helper function to continue chaining intermediate results.
+  var result = function(instance, obj) {
+    return instance._chain ? _(obj).chain() : obj;
+  };
+
+  // Add your own custom functions to the Underscore object.
+  _.mixin = function(obj) {
+    _.each(_.functions(obj), function(name) {
+      var func = _[name] = obj[name];
+      _.prototype[name] = function() {
+        var args = [this._wrapped];
+        push.apply(args, arguments);
+        return result(this, func.apply(_, args));
+      };
+    });
+  };
+
+  // Add all of the Underscore functions to the wrapper object.
+  _.mixin(_);
+
+  // Add all mutator Array functions to the wrapper.
+  _.each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) {
+    var method = ArrayProto[name];
+    _.prototype[name] = function() {
+      var obj = this._wrapped;
+      method.apply(obj, arguments);
+      if ((name === 'shift' || name === 'splice') && obj.length === 0) delete obj[0];
+      return result(this, obj);
+    };
+  });
+
+  // Add all accessor Array functions to the wrapper.
+  _.each(['concat', 'join', 'slice'], function(name) {
+    var method = ArrayProto[name];
+    _.prototype[name] = function() {
+      return result(this, method.apply(this._wrapped, arguments));
+    };
+  });
+
+  // Extracts the result from a wrapped and chained object.
+  _.prototype.value = function() {
+    return this._wrapped;
+  };
+
+  // Provide unwrapping proxy for some methods used in engine operations
+  // such as arithmetic and JSON stringification.
+  _.prototype.valueOf = _.prototype.toJSON = _.prototype.value;
+
+  _.prototype.toString = function() {
+    return '' + this._wrapped;
+  };
+
+  // AMD registration happens at the end for compatibility with AMD loaders
+  // that may not enforce next-turn semantics on modules. Even though general
+  // practice for AMD registration is to be anonymous, underscore registers
+  // as a named module because, like jQuery, it is a base library that is
+  // popular enough to be bundled in a third party lib, but not be part of
+  // an AMD load request. Those cases could generate an error when an
+  // anonymous define() is called outside of a loader request.
+  if (typeof define === 'function' && define.amd) {
+    define('underscore', [], function() {
+      return _;
+    });
+  }
+}.call(this));

+ 6 - 3
src/http/index.html

@@ -2,7 +2,11 @@
  <head>
    <script src="bootstrap.bundle.min.js"></script>
    <link rel="stylesheet" style="text/css" href="bootstrap.min.css" />
+   <script src="deps/jquery.min.js"></script>
+   <script src="deps/underscore.js"></script>
+   <script src="jsonform.js"></script>
    <script src="portal.js"></script>
+   <title>Raspi Bootstrap Captive</title>
  </head>
  <body>
    <div class="container-fluid">
@@ -11,9 +15,8 @@
        <div id="progress_bar" class="progress-bar" role="progressbar" aria-valuenow="100" aria-valuemin="0" aria-valuemax="100" style="width: 100%"></div>
      </div>
      <div class="form-group">
-       <input id="input_ssid" placeholder="SSID" class="form-control">
-       <input id="input_pw" placeholder="Password" class="form-control">
-       <button id="wifi_button" onclick="portal.setWifi()" class="btn btn-primary btn-lg form-control">set wifi</button>
+       <div id="form_div">
+       </div>
      </div>
      <div class="form-group">
        <button onclick="portal.disablePortal()" class="btn btn-warning btn-lg form-control">Disable Portal</button>

+ 3734 - 0
src/http/jsonform.js

@@ -0,0 +1,3734 @@
+/* Copyright (c) 2012 Joshfire - MIT license */
+/**
+ * @fileoverview Core of the JSON Form client-side library.
+ *
+ * Generates an HTML form from a structured data model and a layout description.
+ *
+ * The library may also validate inputs entered by the user against the data model
+ * upon form submission and create the structured data object initialized with the
+ * values that were submitted.
+ *
+ * The library depends on:
+ *  - jQuery
+ *  - the underscore library
+ *  - a JSON parser/serializer. Nothing to worry about in modern browsers.
+ *  - the JSONFormValidation library (in jsv.js) for validation purpose
+ *
+ * See documentation at:
+ * http://developer.joshfire.com/doc/dev/ref/jsonform
+ *
+ * The library creates and maintains an internal data tree along with the DOM.
+ * That structure is necessary to handle arrays (and nested arrays!) that are
+ * dynamic by essence.
+ */
+
+ /*global window*/
+
+(function(serverside, global, $, _, JSON) {
+  if (serverside && !_) {
+    _ = require('underscore');
+  }
+
+  /**
+   * Regular expressions used to extract array indexes in input field names
+   */
+  var reArray = /\[([0-9]*)\](?=\[|\.|$)/g;
+
+  /**
+   * Template settings for form views
+   */
+  var fieldTemplateSettings = {
+    evaluate    : /<%([\s\S]+?)%>/g,
+    interpolate : /<%=([\s\S]+?)%>/g
+  };
+
+  /**
+   * Template settings for value replacement
+   */
+  var valueTemplateSettings = {
+    evaluate    : /\{\[([\s\S]+?)\]\}/g,
+    interpolate : /\{\{([\s\S]+?)\}\}/g
+  };
+
+  /**
+   * Returns true if given value is neither "undefined" nor null
+   */
+  var isSet = function (value) {
+    return !(_.isUndefined(value) || _.isNull(value));
+  };
+
+  /**
+   * Returns true if given property is directly property of an object
+   */
+  var hasOwnProperty = function (obj, prop) {
+    return typeof obj === 'object' && obj.hasOwnProperty(prop);
+  }
+
+  /**
+   * The jsonform object whose methods will be exposed to the window object
+   */
+  var jsonform = {util:{}};
+
+
+  // From backbonejs
+  var escapeHTML = function (string) {
+    if (!isSet(string)) {
+      return '';
+    }
+    string = '' + string;
+    if (!string) {
+      return '';
+    }
+    return string
+      .replace(/&(?!\w+;|#\d+;|#x[\da-f]+;)/gi, '&amp;')
+      .replace(/</g, '&lt;')
+      .replace(/>/g, '&gt;')
+      .replace(/"/g, '&quot;')
+      .replace(/'/g, '&#x27;')
+      .replace(/\//g, '&#x2F;');
+  };
+
+/**
+ * Escapes selector name for use with jQuery
+ *
+ * All meta-characters listed in jQuery doc are escaped:
+ * http://api.jquery.com/category/selectors/
+ *
+ * @function
+ * @param {String} selector The jQuery selector to escape
+ * @return {String} The escaped selector.
+ */
+var escapeSelector = function (selector) {
+  return selector.replace(/([ \!\"\#\$\%\&\'\(\)\*\+\,\.\/\:\;<\=\>\?\@\[\\\]\^\`\{\|\}\~])/g, '\\$1');
+};
+
+/**
+ *
+ * Slugifies a string by replacing spaces with _. Used to create
+ * valid classnames and ids for the form.
+ *
+ * @function
+ * @param {String} str The string to slugify
+ * @return {String} The slugified string.
+ */
+var slugify = function(str) {
+  return str.replace(/\ /g, '_');
+}
+
+/**
+ * Initializes tabular sections in forms. Such sections are generated by the
+ * 'selectfieldset' type of elements in JSON Form.
+ *
+ * Input fields that are not visible are automatically disabled
+ * not to appear in the submitted form. That's on purpose, as tabs
+ * are meant to convey an alternative (and not a sequence of steps).
+ *
+ * The tabs menu is not rendered as tabs but rather as a select field because
+ * it's easier to grasp that it's an alternative.
+ *
+ * Code based on bootstrap-tabs.js, updated to:
+ * - react to option selection instead of tab click
+ * - disable input fields in non visible tabs
+ * - disable the possibility to have dropdown menus (no meaning here)
+ * - act as a regular function instead of as a jQuery plug-in.
+ *
+ * @function
+ * @param {Object} tabs jQuery object that contains the tabular sections
+ *  to initialize. The object may reference more than one element.
+ */
+var initializeTabs = function (tabs) {
+  var activate = function (element, container) {
+    container
+      .find('> .active')
+      .removeClass('active');
+    element.addClass('active');
+  };
+
+  var enableFields = function ($target, targetIndex) {
+    // Enable all fields in the targeted tab
+    $target.find('input, textarea, select').removeAttr('disabled');
+
+    // Disable all fields in other tabs
+    $target.parent()
+      .children(':not([data-idx=' + targetIndex + '])')
+      .find('input, textarea, select')
+      .attr('disabled', 'disabled');
+  };
+
+  var optionSelected = function (e) {
+    var $option = $("option:selected", $(this)),
+      $select = $(this),
+      // do not use .attr() as it sometimes unexplicably fails
+      targetIdx = $option.get(0).getAttribute('data-idx') || $option.attr('value'),
+      $target;
+
+    e.preventDefault();
+    if ($option.hasClass('active')) {
+      return;
+    }
+
+    $target = $(this).parents('.tabbable').eq(0).find('> .tab-content > [data-idx=' + targetIdx + ']');
+
+    activate($option, $select);
+    activate($target, $target.parent());
+    enableFields($target, targetIdx);
+  };
+
+  var tabClicked = function (e) {
+    var $a = $('a', $(this));
+    var $content = $(this).parents('.tabbable').first()
+      .find('.tab-content').first();
+    var targetIdx = $(this).index();
+    // The `>` here is to prevent activating selectfieldsets inside a tabarray
+    var $target = $content.find('> [data-idx=' + targetIdx + ']');
+
+    e.preventDefault();
+    activate($(this), $(this).parent());
+    activate($target, $target.parent());
+    if ($(this).parent().hasClass('jsonform-alternative')) {
+      enableFields($target, targetIdx);
+    }
+  };
+
+  tabs.each(function () {
+    $(this).delegate('select.nav', 'change', optionSelected);
+    $(this).find('select.nav').each(function () {
+      $(this).val($(this).find('.active').attr('value'));
+      // do not use .attr() as it sometimes unexplicably fails
+      var targetIdx = $(this).find('option:selected').get(0).getAttribute('data-idx') ||
+        $(this).find('option:selected').attr('value');
+      var $target = $(this).parents('.tabbable').eq(0).find('> .tab-content > [data-idx=' + targetIdx + ']');
+      enableFields($target, targetIdx);
+    });
+
+    $(this).delegate('ul.nav li', 'click', tabClicked);
+    $(this).find('ul.nav li.active').click();
+  });
+};
+
+
+// Twitter bootstrap-friendly HTML boilerplate for standard inputs
+jsonform.fieldTemplate = function(inner) {
+  return '<div ' +
+    '<% for(var key in elt.htmlMetaData) {%>' +
+      '<%= key %>="<%= elt.htmlMetaData[key] %>" ' +
+    '<% }%>' +
+    'class="form-group jsonform-error-<%= keydash %>' +
+    '<%= elt.htmlClass ? " " + elt.htmlClass : "" %>' +
+    '<%= (node.schemaElement && node.schemaElement.required && (node.schemaElement.type !== "boolean") ? " jsonform-required" : "") %>' +
+    '<%= (node.readOnly ? " jsonform-readonly" : "") %>' +
+    '<%= (node.disabled ? " jsonform-disabled" : "") %>' +
+    '">' +
+    '<% if (!elt.notitle) { %>' +
+      '<label for="<%= node.id %>"><%= node.title ? node.title : node.name %></label>' +
+    '<% } %>' +
+    '<div class="controls">' +
+      '<% if (node.prepend || node.append) { %>' +
+      '<div class="<% if (node.prepend) { %>input-group<% } %>' +
+        '<% if (node.append) { %> input-group<% } %>">' +
+        '<% if (node.prepend) { %>' +
+          '<span class="input-group-addon"><%= node.prepend %></span>' +
+        '<% } %>' +
+      '<% } %>' +
+      inner +
+      '<% if (node.append) { %>' +
+        '<span class="input-group-addon"><%= node.append %></span>' +
+      '<% } %>' +
+      '<% if (node.prepend || node.append) { %>' +
+        '</div>' +
+      '<% } %>' +
+      '<% if (node.description) { %>' +
+        '<span class="help-block"><%= node.description %></span>' +
+      '<% } %>' +
+      '<span class="help-block jsonform-errortext" style="display:none;"></span>' +
+    '</div></div>';
+};
+
+var fileDisplayTemplate = '<div class="_jsonform-preview">' +
+  '<% if (value.type=="image") { %>' +
+  '<img class="jsonform-preview" id="jsonformpreview-<%= id %>" src="<%= value.url %>" />' +
+  '<% } else { %>' +
+  '<a href="<%= value.url %>"><%= value.name %></a> (<%= Math.ceil(value.size/1024) %>kB)' +
+  '<% } %>' +
+  '</div>' +
+  '<a href="#" class="btn btn-default _jsonform-delete"><i class="glyphicon glyphicon-remove" title="Remove"></i></a> ';
+
+var inputFieldTemplate = function (type) {
+  return {
+    'template': '<input type="' + type + '" ' +
+      'class=\'form-control<%= (fieldHtmlClass ? " " + fieldHtmlClass : "") %>\'' +
+      'name="<%= node.name %>" value="<%= escape(value) %>" id="<%= id %>"' +
+      '<%= (node.disabled? " disabled" : "")%>' +
+      '<%= (node.readOnly ? " readonly=\'readonly\'" : "") %>' +
+      '<%= (node.schemaElement && (node.schemaElement.step > 0 || node.schemaElement.step == "any") ? " step=\'" + node.schemaElement.step + "\'" : "") %>' +
+      '<%= (node.schemaElement && node.schemaElement.maxLength ? " maxlength=\'" + node.schemaElement.maxLength + "\'" : "") %>' +
+      '<%= (node.schemaElement && node.schemaElement.required && (node.schemaElement.type !== "boolean") ? " required=\'required\'" : "") %>' +
+      '<%= (node.placeholder? " placeholder=" + \'"\' + escape(node.placeholder) + \'"\' : "")%>' +
+      ' />',
+    'fieldtemplate': true,
+    'inputfield': true
+  }
+};
+
+jsonform.elementTypes = {
+  'none': {
+    'template': ''
+  },
+  'root': {
+    'template': '<div><%= children %></div>'
+  },
+  'text': inputFieldTemplate('text'),
+  'password': inputFieldTemplate('password'),
+  'date': inputFieldTemplate('date'),
+  'datetime': inputFieldTemplate('datetime'),
+  'datetime-local': inputFieldTemplate('datetime-local'),
+  'email': inputFieldTemplate('email'),
+  'month': inputFieldTemplate('month'),
+  'number': inputFieldTemplate('number'),
+  'search': inputFieldTemplate('search'),
+  'tel': inputFieldTemplate('tel'),
+  'time': inputFieldTemplate('time'),
+  'url': inputFieldTemplate('url'),
+  'week': inputFieldTemplate('week'),
+  'range': {
+    'template': '<input type="range" ' +
+      '<%= (fieldHtmlClass ? "class=\'" + fieldHtmlClass + "\' " : "") %>' +
+      'name="<%= node.name %>" value="<%= escape(value) %>" id="<%= id %>"' +
+      '<%= (node.disabled? " disabled" : "")%>' +
+      ' min=<%= range.min %>' +
+      ' max=<%= range.max %>' +
+      ' step=<%= range.step %>' +
+      '<%= (node.schemaElement && node.schemaElement.required ? " required=\'required\'" : "") %>' +
+      ' />',
+    'fieldtemplate': true,
+    'inputfield': true,
+    'onBeforeRender': function (data, node) {
+      data.range = {
+        min: 1,
+        max: 100,
+        step: 1
+      };
+      if (!node || !node.schemaElement) return;
+      if (node.formElement && node.formElement.step) {
+        data.range.step = node.formElement.step;
+      }
+      if (typeof node.schemaElement.minimum !== 'undefined') {
+        if (node.schemaElement.exclusiveMinimum) {
+          data.range.min = node.schemaElement.minimum + data.range.step;
+        }
+        else {
+          data.range.min = node.schemaElement.minimum;
+        }
+      }
+      if (typeof node.schemaElement.maximum !== 'undefined') {
+        if (node.schemaElement.exclusiveMaximum) {
+          data.range.max = node.schemaElement.maximum - data.range.step;
+        }
+        else {
+          data.range.max = node.schemaElement.maximum;
+        }
+      }
+    }
+  },
+  'color':{
+    'template':'<input type="text" ' +
+      '<%= (fieldHtmlClass ? "class=\'" + fieldHtmlClass + "\' " : "") %>' +
+      'name="<%= node.name %>" value="<%= escape(value) %>" id="<%= id %>"' +
+      '<%= (node.disabled? " disabled" : "")%>' +
+      '<%= (node.schemaElement && node.schemaElement.required ? " required=\'required\'" : "") %>' +
+      ' />',
+    'fieldtemplate': true,
+    'inputfield': true,
+    'onInsert': function(evt, node) {
+      $(node.el).find('#' + escapeSelector(node.id)).spectrum({
+        preferredFormat: "hex",
+        showInput: true
+      });
+    }
+  },
+  'textarea':{
+    'template':'<textarea id="<%= id %>" name="<%= node.name %>" ' +
+      '<%= (fieldHtmlClass ? "class=\'" + fieldHtmlClass + "\' " : "") %>' +
+      'style="height:<%= elt.height || "150px" %>;width:<%= elt.width || "100%" %>;"' +
+      '<%= (node.disabled? " disabled" : "")%>' +
+      '<%= (node.readOnly ? " readonly=\'readonly\'" : "") %>' +
+      '<%= (node.schemaElement && node.schemaElement.maxLength ? " maxlength=\'" + node.schemaElement.maxLength + "\'" : "") %>' +
+      '<%= (node.schemaElement && node.schemaElement.required ? " required=\'required\'" : "") %>' +
+      '<%= (node.placeholder? " placeholder=" + \'"\' + escape(node.placeholder) + \'"\' : "")%>' +
+      '><%= value %></textarea>',
+    'fieldtemplate': true,
+    'inputfield': true
+  },
+  'wysihtml5':{
+    'template':'<textarea id="<%= id %>" name="<%= node.name %>" style="height:<%= elt.height || "300px" %>;width:<%= elt.width || "100%" %>;"' +
+      '<%= (fieldHtmlClass ? "class=\'" + fieldHtmlClass + "\' " : "") %>' +
+      '<%= (node.disabled? " disabled" : "")%>' +
+      '<%= (node.readOnly ? " readonly=\'readonly\'" : "") %>' +
+      '<%= (node.schemaElement && node.schemaElement.maxLength ? " maxlength=\'" + node.schemaElement.maxLength + "\'" : "") %>' +
+      '<%= (node.schemaElement && node.schemaElement.required ? " required=\'required\'" : "") %>' +
+      '<%= (node.placeholder? " placeholder=" + \'"\' + escape(node.placeholder) + \'"\' : "")%>' +
+      '><%= value %></textarea>',
+    'fieldtemplate': true,
+    'inputfield': true,
+    'onInsert': function (evt, node) {
+      var setup = function () {
+        //protect from double init
+        if ($(node.el).data("wysihtml5")) return;
+        $(node.el).data("wysihtml5_loaded",true);
+
+        $(node.el).find('#' + escapeSelector(node.id)).wysihtml5({
+          "html": true,
+          "link": true,
+          "font-styles":true,
+          "image": false,
+          "events": {
+            "load": function () {
+              // In chrome, if an element is required and hidden, it leads to
+              // the error 'An invalid form control with name='' is not focusable'
+              // See http://stackoverflow.com/questions/7168645/invalid-form-control-only-in-google-chrome
+              $(this.textareaElement).removeAttr('required');
+            }
+          }
+        });
+      };
+
+      // Is there a setup hook?
+      if (window.jsonform_wysihtml5_setup) {
+        window.jsonform_wysihtml5_setup(setup);
+        return;
+      }
+
+      // Wait until wysihtml5 is loaded
+      var itv = window.setInterval(function() {
+        if (window.wysihtml5) {
+          window.clearInterval(itv);
+          setup();
+        }
+      },1000);
+    }
+  },
+  'ace':{
+    'template':'<div id="<%= id %>" style="position:relative;height:<%= elt.height || "300px" %>;"><div id="<%= id %>__ace" style="width:<%= elt.width || "100%" %>;height:<%= elt.height || "300px" %>;"></div><input type="hidden" name="<%= node.name %>" id="<%= id %>__hidden" value="<%= escape(value) %>"/></div>',
+    'fieldtemplate': true,
+    'inputfield': true,
+    'onInsert': function (evt, node) {
+      var setup = function () {
+        var formElement = node.formElement || {};
+        var ace = window.ace;
+        var editor = ace.edit($(node.el).find('#' + escapeSelector(node.id) + '__ace').get(0));
+        var idSelector = '#' + escapeSelector(node.id) + '__hidden';
+        // Force editor to use "\n" for new lines, not to bump into ACE "\r" conversion issue
+        // (ACE is ok with "\r" on pasting but fails to return "\r" when value is extracted)
+        editor.getSession().setNewLineMode('unix');
+        editor.renderer.setShowPrintMargin(false);
+        editor.setTheme("ace/theme/"+(formElement.aceTheme||"twilight"));
+
+        if (formElement.aceMode) {
+          editor.getSession().setMode("ace/mode/"+formElement.aceMode);
+        }
+        editor.getSession().setTabSize(2);
+
+        // Set the contents of the initial manifest file
+        editor.getSession().setValue(node.value||"");
+
+        //TODO this is clearly sub-optimal
+        // 'Lazily' bind to the onchange 'ace' event to give
+        // priority to user edits
+        var lazyChanged = _.debounce(function () {
+          $(node.el).find(idSelector).val(editor.getSession().getValue());
+          $(node.el).find(idSelector).change();
+        }, 600);
+        editor.getSession().on('change', lazyChanged);
+
+        editor.on('blur', function() {
+          $(node.el).find(idSelector).change();
+          $(node.el).find(idSelector).trigger("blur");
+        });
+        editor.on('focus', function() {
+          $(node.el).find(idSelector).trigger("focus");
+        });
+      };
+
+      // Is there a setup hook?
+      if (window.jsonform_ace_setup) {
+        window.jsonform_ace_setup(setup);
+        return;
+      }
+
+      // Wait until ACE is loaded
+      var itv = window.setInterval(function() {
+        if (window.ace) {
+          window.clearInterval(itv);
+          setup();
+        }
+      },1000);
+    }
+  },
+  'checkbox':{
+    'template': '<div class="checkbox"><label><input type="checkbox" id="<%= id %>" ' +
+      '<%= (fieldHtmlClass ? " class=\'" + fieldHtmlClass + "\'": "") %>' +
+      'name="<%= node.name %>" value="1" <% if (value) {%>checked<% } %>' +
+      '<%= (node.disabled? " disabled" : "")%>' +
+      '<%= (node.schemaElement && node.schemaElement.required && (node.schemaElement.type !== "boolean") ? " required=\'required\'" : "") %>' +
+      ' /><%= node.inlinetitle || "" %>' +
+      '</label></div>',
+    'fieldtemplate': true,
+    'inputfield': true,
+    'getElement': function (el) {
+      return $(el).parent().get(0);
+    }
+  },
+  'file':{
+    'template':'<input class="input-file" id="<%= id %>" name="<%= node.name %>" type="file" ' +
+      '<%= (node.schemaElement && node.schemaElement.required ? " required=\'required\'" : "") %>' +
+      '/>',
+    'fieldtemplate': true,
+    'inputfield': true
+  },
+  'file-hosted-public':{
+    'template':'<span><% if (value && (value.type||value.url)) { %>'+fileDisplayTemplate+'<% } %><input class="input-file" id="_transloadit_<%= id %>" type="file" name="<%= transloaditname %>" /><input data-transloadit-name="_transloadit_<%= transloaditname %>" type="hidden" id="<%= id %>" name="<%= node.name %>" value=\'<%= escape(JSON.stringify(node.value)) %>\' /></span>',
+    'fieldtemplate': true,
+    'inputfield': true,
+    'getElement': function (el) {
+      return $(el).parent().get(0);
+    },
+    'onBeforeRender': function (data, node) {
+
+      if (!node.ownerTree._transloadit_generic_public_index) {
+        node.ownerTree._transloadit_generic_public_index=1;
+      } else {
+        node.ownerTree._transloadit_generic_public_index++;
+      }
+
+      data.transloaditname = "_transloadit_jsonform_genericupload_public_"+node.ownerTree._transloadit_generic_public_index;
+
+      if (!node.ownerTree._transloadit_generic_elts) node.ownerTree._transloadit_generic_elts = {};
+      node.ownerTree._transloadit_generic_elts[data.transloaditname] = node;
+    },
+    'onChange': function(evt,elt) {
+      // The "transloadit" function should be called only once to enable
+      // the service when the form is submitted. Has it already been done?
+      if (elt.ownerTree._transloadit_bound) {
+        return false;
+      }
+      elt.ownerTree._transloadit_bound = true;
+
+      // Call the "transloadit" function on the form element
+      var formElt = $(elt.ownerTree.domRoot);
+      formElt.transloadit({
+        autoSubmit: false,
+        wait: true,
+        onSuccess: function (assembly) {
+          // Image has been uploaded. Check the "results" property that
+          // contains the list of files that Transloadit produced. There
+          // should be one image per file input in the form at most.
+          // console.log(assembly.results);
+          var results = _.values(assembly.results);
+          results = _.flatten(results);
+          _.each(results, function (result) {
+            // Save the assembly result in the right hidden input field
+            var id = elt.ownerTree._transloadit_generic_elts[result.field].id;
+            var input = formElt.find('#' + escapeSelector(id));
+            var nonEmptyKeys = _.filter(_.keys(result.meta), function (key) {
+              return !!isSet(result.meta[key]);
+            });
+            result.meta = _.pick(result.meta, nonEmptyKeys);
+            input.val(JSON.stringify(result));
+          });
+
+          // Unbind transloadit from the form
+          elt.ownerTree._transloadit_bound = false;
+          formElt.unbind('submit.transloadit');
+
+          // Submit the form on next tick
+          _.delay(function () {
+            console.log('submit form');
+            elt.ownerTree.submit();
+          }, 10);
+        },
+        onError: function (assembly) {
+          // TODO: report the error to the user
+          console.log('assembly error', assembly);
+        }
+      });
+    },
+    'onInsert': function (evt, node) {
+      $(node.el).find('a._jsonform-delete').on('click', function (evt) {
+        $(node.el).find('._jsonform-preview').remove();
+        $(node.el).find('a._jsonform-delete').remove();
+        $(node.el).find('#' + escapeSelector(node.id)).val('');
+        evt.preventDefault();
+        return false;
+      });
+    },
+    'onSubmit':function(evt, elt) {
+      if (elt.ownerTree._transloadit_bound) {
+        return false;
+      }
+      return true;
+    }
+
+  },
+  'file-transloadit': {
+    'template': '<span><% if (value && (value.type||value.url)) { %>'+fileDisplayTemplate+'<% } %><input class="input-file" id="_transloadit_<%= id %>" type="file" name="_transloadit_<%= node.name %>" /><input type="hidden" id="<%= id %>" name="<%= node.name %>" value=\'<%= escape(JSON.stringify(node.value)) %>\' /></span>',
+    'fieldtemplate': true,
+    'inputfield': true,
+    'getElement': function (el) {
+      return $(el).parent().get(0);
+    },
+    'onChange': function (evt, elt) {
+      // The "transloadit" function should be called only once to enable
+      // the service when the form is submitted. Has it already been done?
+      if (elt.ownerTree._transloadit_bound) {
+        return false;
+      }
+      elt.ownerTree._transloadit_bound = true;
+
+      // Call the "transloadit" function on the form element
+      var formElt = $(elt.ownerTree.domRoot);
+      formElt.transloadit({
+        autoSubmit: false,
+        wait: true,
+        onSuccess: function (assembly) {
+          // Image has been uploaded. Check the "results" property that
+          // contains the list of files that Transloadit produced. Note
+          // JSONForm only supports 1-to-1 associations, meaning it
+          // expects the "results" property to contain only one image
+          // per file input in the form.
+          // console.log(assembly.results);
+          var results = _.values(assembly.results);
+          results = _.flatten(results);
+          _.each(results, function (result) {
+            // Save the assembly result in the right hidden input field
+            var input = formElt.find('input[name="' +
+              result.field.replace(/^_transloadit_/, '') +
+              '"]');
+            var nonEmptyKeys = _.filter(_.keys(result.meta), function (key) {
+              return !!isSet(result.meta[key]);
+            });
+            result.meta = _.pick(result.meta, nonEmptyKeys);
+            input.val(JSON.stringify(result));
+          });
+
+          // Unbind transloadit from the form
+          elt.ownerTree._transloadit_bound = false;
+          formElt.unbind('submit.transloadit');
+
+          // Submit the form on next tick
+          _.delay(function () {
+            console.log('submit form');
+            elt.ownerTree.submit();
+          }, 10);
+        },
+        onError: function (assembly) {
+          // TODO: report the error to the user
+          console.log('assembly error', assembly);
+        }
+      });
+    },
+    'onInsert': function (evt, node) {
+      $(node.el).find('a._jsonform-delete').on('click', function (evt) {
+        $(node.el).find('._jsonform-preview').remove();
+        $(node.el).find('a._jsonform-delete').remove();
+        $(node.el).find('#' + escapeSelector(node.id)).val('');
+        evt.preventDefault();
+        return false;
+      });
+    },
+    'onSubmit': function (evt, elt) {
+      if (elt.ownerTree._transloadit_bound) {
+        return false;
+      }
+      return true;
+    }
+  },
+  'select':{
+    'template':'<select name="<%= node.name %>" id="<%= id %>"' +
+      'class=\'form-control<%= (fieldHtmlClass ? " " + fieldHtmlClass : "") %>\'' +
+      '<%= (node.schemaElement && node.schemaElement.disabled? " disabled" : "")%>' +
+      '<%= (node.schemaElement && node.schemaElement.required ? " required=\'required\'" : "") %>' +
+      '> ' +
+      '<% _.each(node.options, function(key, val) { if(key instanceof Object) { if (value === key.value) { %> <option selected value="<%= key.value %>"><%= key.title %></option> <% } else { %> <option value="<%= key.value %>"><%= key.title %></option> <% }} else { if (value === key) { %> <option selected value="<%= key %>"><%= key %></option> <% } else { %><option value="<%= key %>"><%= key %></option> <% }}}); %> ' +
+      '</select>',
+    'fieldtemplate': true,
+    'inputfield': true
+  },
+  'imageselect': {
+    'template': '<div>' +
+      '<input type="hidden" name="<%= node.name %>" id="<%= node.id %>" value="<%= value %>" />' +
+      '<div class="dropdown">' +
+      '<a class="btn<% if (buttonClass && node.value) { %> <%= buttonClass %><% } else { %> btn-default<% } %>" data-toggle="dropdown" href="#"<% if (node.value) { %> style="max-width:<%= width %>px;max-height:<%= height %>px"<% } %>>' +
+        '<% if (node.value) { %><img src="<% if (!node.value.match(/^https?:/)) { %><%= prefix %><% } %><%= node.value %><%= suffix %>" alt="" /><% } else { %><%= buttonTitle %><% } %>' +
+      '</a>' +
+      '<div class="dropdown-menu navbar" id="<%= node.id %>_dropdown">' +
+        '<div>' +
+        '<% _.each(node.options, function(key, idx) { if ((idx > 0) && ((idx % columns) === 0)) { %></div><div><% } %><a class="btn<% if (buttonClass) { %> <%= buttonClass %><% } else { %> btn-default<% } %>" style="max-width:<%= width %>px;max-height:<%= height %>px"><% if (key instanceof Object) { %><img src="<% if (!key.value.match(/^https?:/)) { %><%= prefix %><% } %><%= key.value %><%= suffix %>" alt="<%= key.title %>" /></a><% } else { %><img src="<% if (!key.match(/^https?:/)) { %><%= prefix %><% } %><%= key %><%= suffix %>" alt="" /><% } %></a> <% }); %>' +
+        '</div>' +
+        '<div class="pagination-right"><a class="btn btn-default">Reset</a></div>' +
+      '</div>' +
+      '</div>' +
+      '</div>',
+    'fieldtemplate': true,
+    'inputfield': true,
+    'onBeforeRender': function (data, node) {
+      var elt = node.formElement || {};
+      var nbRows = null;
+      var maxColumns = elt.imageSelectorColumns || 5;
+      data.buttonTitle = elt.imageSelectorTitle || 'Select...';
+      data.prefix = elt.imagePrefix || '';
+      data.suffix = elt.imageSuffix || '';
+      data.width = elt.imageWidth || 32;
+      data.height = elt.imageHeight || 32;
+      data.buttonClass = elt.imageButtonClass || false;
+      if (node.options.length > maxColumns) {
+        nbRows = Math.ceil(node.options.length / maxColumns);
+        data.columns = Math.ceil(node.options.length / nbRows);
+      }
+      else {
+        data.columns = maxColumns;
+      }
+    },
+    'getElement': function (el) {
+      return $(el).parent().get(0);
+    },
+    'onInsert': function (evt, node) {
+      $(node.el).on('click', '.dropdown-menu a', function (evt) {
+        evt.preventDefault();
+        evt.stopPropagation();
+        var img = (evt.target.nodeName.toLowerCase() === 'img') ?
+          $(evt.target) :
+          $(evt.target).find('img');
+        var value = img.attr('src');
+        var elt = node.formElement || {};
+        var prefix = elt.imagePrefix || '';
+        var suffix = elt.imageSuffix || '';
+        var width = elt.imageWidth || 32;
+        var height = elt.imageHeight || 32;
+        if (value) {
+          if (value.indexOf(prefix) === 0) {
+            value = value.substring(prefix.length);
+          }
+          value = value.substring(0, value.length - suffix.length);
+          $(node.el).find('input').attr('value', value);
+          $(node.el).find('a[data-toggle="dropdown"]')
+            .addClass(elt.imageButtonClass)
+            .attr('style', 'max-width:' + width + 'px;max-height:' + height + 'px')
+            .html('<img src="' + (!value.match(/^https?:/) ? prefix : '') + value + suffix + '" alt="" />');
+        }
+        else {
+          $(node.el).find('input').attr('value', '');
+          $(node.el).find('a[data-toggle="dropdown"]')
+            .removeClass(elt.imageButtonClass)
+            .removeAttr('style')
+            .html(elt.imageSelectorTitle || 'Select...');
+        }
+      });
+    }
+  },
+  'iconselect': {
+    'template': '<div>' +
+      '<input type="hidden" name="<%= node.name %>" id="<%= node.id %>" value="<%= value %>" />' +
+      '<div class="dropdown">' +
+      '<a class="btn<% if (buttonClass && node.value) { %> <%= buttonClass %><% } %>" data-toggle="dropdown" href="#"<% if (node.value) { %> style="max-width:<%= width %>px;max-height:<%= height %>px"<% } %>>' +
+        '<% if (node.value) { %><i class="icon-<%= node.value %>" /><% } else { %><%= buttonTitle %><% } %>' +
+      '</a>' +
+      '<div class="dropdown-menu navbar" id="<%= node.id %>_dropdown">' +
+        '<div>' +
+        '<% _.each(node.options, function(key, idx) { if ((idx > 0) && ((idx % columns) === 0)) { %></div><div><% } %><a class="btn<% if (buttonClass) { %> <%= buttonClass %><% } %>" ><% if (key instanceof Object) { %><i class="icon-<%= key.value %>" alt="<%= key.title %>" /></a><% } else { %><i class="icon-<%= key %>" alt="" /><% } %></a> <% }); %>' +
+        '</div>' +
+        '<div class="pagination-right"><a class="btn">Reset</a></div>' +
+      '</div>' +
+      '</div>' +
+      '</div>',
+    'fieldtemplate': true,
+    'inputfield': true,
+    'onBeforeRender': function (data, node) {
+      var elt = node.formElement || {};
+      var nbRows = null;
+      var maxColumns = elt.imageSelectorColumns || 5;
+      data.buttonTitle = elt.imageSelectorTitle || 'Select...';
+      data.buttonClass = elt.imageButtonClass || false;
+      if (node.options.length > maxColumns) {
+        nbRows = Math.ceil(node.options.length / maxColumns);
+        data.columns = Math.ceil(node.options.length / nbRows);
+      }
+      else {
+        data.columns = maxColumns;
+      }
+    },
+    'getElement': function (el) {
+      return $(el).parent().get(0);
+    },
+    'onInsert': function (evt, node) {
+      $(node.el).on('click', '.dropdown-menu a', function (evt) {
+        evt.preventDefault();
+        evt.stopPropagation();
+        var i = (evt.target.nodeName.toLowerCase() === 'i') ?
+          $(evt.target) :
+          $(evt.target).find('i');
+        var value = i.attr('class');
+        var elt = node.formElement || {};
+        if (value) {
+          value = value;
+          $(node.el).find('input').attr('value', value);
+          $(node.el).find('a[data-toggle="dropdown"]')
+            .addClass(elt.imageButtonClass)
+            .html('<i class="'+ value +'" alt="" />');
+        }
+        else {
+          $(node.el).find('input').attr('value', '');
+          $(node.el).find('a[data-toggle="dropdown"]')
+            .removeClass(elt.imageButtonClass)
+            .html(elt.imageSelectorTitle || 'Select...');
+        }
+      });
+    }
+  },
+  'radios':{
+    'template': '<div id="<%= node.id %>"><% _.each(node.options, function(key, val) { %><div class="radio"><label><input<%= (fieldHtmlClass ? " class=\'" + fieldHtmlClass + "\'": "") %> type="radio" <% if (((key instanceof Object) && (value === key.value)) || (value === key)) { %> checked="checked" <% } %> name="<%= node.name %>" value="<%= (key instanceof Object ? key.value : key) %>"' +
+      '<%= (node.disabled? " disabled" : "")%>' +
+      '<%= (node.schemaElement && node.schemaElement.required ? " required=\'required\'" : "") %>' +
+      '/><%= (key instanceof Object ? key.title : key) %></label></div> <% }); %></div>',
+    'fieldtemplate': true,
+    'inputfield': true
+  },
+  'radiobuttons': {
+    'template': '<div id="<%= node.id %>">' +
+      '<% _.each(node.options, function(key, val) { %>' +
+        '<label class="btn btn-default">' +
+        '<input<%= (fieldHtmlClass ? " class=\'" + fieldHtmlClass + "\'": "") %> type="radio" style="position:absolute;left:-9999px;" ' +
+        '<% if (((key instanceof Object) && (value === key.value)) || (value === key)) { %> checked="checked" <% } %> name="<%= node.name %>" value="<%= (key instanceof Object ? key.value : key) %>" />' +
+        '<span><%= (key instanceof Object ? key.title : key) %></span></label> ' +
+        '<% }); %>' +
+      '</div>',
+    'fieldtemplate': true,
+    'inputfield': true,
+    'onInsert': function (evt, node) {
+      var activeClass = 'active';
+      var elt = node.formElement || {};
+      if (elt.activeClass) {
+        activeClass += ' ' + elt.activeClass;
+      }
+      $(node.el).find('label').on('click', function () {
+        $(this).parent().find('label').removeClass(activeClass);
+        $(this).addClass(activeClass);
+      });
+    }
+  },
+  'checkboxes':{
+    'template': '<div><%= choiceshtml %></div>',
+    'fieldtemplate': true,
+    'inputfield': true,
+    'onBeforeRender': function (data, node) {
+      // Build up choices from the enumeration list
+      var choices = null;
+      var choiceshtml = null;
+      var template = '<div class="checkbox"><label>' +
+        '<input type="checkbox" <% if (value) { %> checked="checked" <% } %> name="<%= name %>" value="1"' +
+        '<%= (node.disabled? " disabled" : "")%>' +
+        '/><%= title %></label></div>';
+      if (!node || !node.schemaElement) return;
+
+      if (node.schemaElement.items) {
+        choices =
+          node.schemaElement.items["enum"] ||
+          node.schemaElement.items[0]["enum"];
+      } else {
+        choices = node.schemaElement["enum"];
+      }
+      if (!choices) return;
+
+      choiceshtml = '';
+      _.each(choices, function (choice, idx) {
+        choiceshtml += _.template(template, fieldTemplateSettings)({
+          name: node.key + '[' + idx + ']',
+          value: _.include(node.value, choice),
+          title: hasOwnProperty(node.formElement.titleMap, choice) ? node.formElement.titleMap[choice] : choice,
+          node: node
+        });
+      });
+
+      data.choiceshtml = choiceshtml;
+    }
+  },
+  'array': {
+    'template': '<div id="<%= id %>"><ul class="_jsonform-array-ul" style="list-style-type:none;"><%= children %></ul>' +
+      '<span class="_jsonform-array-buttons">' +
+        '<a href="#" class="btn btn-default _jsonform-array-addmore"><i class="glyphicon glyphicon-plus-sign" title="Add new"></i></a> ' +
+        '<a href="#" class="btn btn-default _jsonform-array-deletelast"><i class="glyphicon glyphicon-minus-sign" title="Delete last"></i></a>' +
+      '</span>' +
+      '</div>',
+    'fieldtemplate': true,
+    'array': true,
+    'childTemplate': function (inner) {
+      if ($('').sortable) {
+        // Insert a "draggable" icon
+        // floating to the left of the main element
+        return '<li data-idx="<%= node.childPos %>">' +
+          '<span class="draggable line"><i class="glyphicon glyphicon-list" title="Move item"></i></span>' +
+          inner +
+          '</li>';
+      }
+      else {
+        return '<li data-idx="<%= node.childPos %>">' +
+          inner +
+          '</li>';
+      }
+    },
+    'onInsert': function (evt, node) {
+      var $nodeid = $(node.el).find('#' + escapeSelector(node.id));
+      var boundaries = node.getArrayBoundaries();
+
+      // Switch two nodes in an array
+      var moveNodeTo = function (fromIdx, toIdx) {
+        // Note "switchValuesWith" extracts values from the DOM since field
+        // values are not synchronized with the tree data structure, so calls
+        // to render are needed at each step to force values down to the DOM
+        // before next move.
+        // TODO: synchronize field values and data structure completely and
+        // call render only once to improve efficiency.
+        if (fromIdx === toIdx) return;
+        var incr = (fromIdx < toIdx) ? 1: -1;
+        var i = 0;
+        var parentEl = $('> ul', $nodeid);
+        for (i = fromIdx; i !== toIdx; i += incr) {
+          node.children[i].switchValuesWith(node.children[i + incr]);
+          node.children[i].render(parentEl.get(0));
+          node.children[i + incr].render(parentEl.get(0));
+        }
+
+        // No simple way to prevent DOM reordering with jQuery UI Sortable,
+        // so we're going to need to move sorted DOM elements back to their
+        // origin position in the DOM ourselves (we switched values but not
+        // DOM elements)
+        var fromEl = $(node.children[fromIdx].el);
+        var toEl = $(node.children[toIdx].el);
+        fromEl.detach();
+        toEl.detach();
+        if (fromIdx < toIdx) {
+          if (fromIdx === 0) parentEl.prepend(fromEl);
+          else $(node.children[fromIdx-1].el).after(fromEl);
+          $(node.children[toIdx-1].el).after(toEl);
+        }
+        else {
+          if (toIdx === 0) parentEl.prepend(toEl);
+          else $(node.children[toIdx-1].el).after(toEl);
+          $(node.children[fromIdx-1].el).after(fromEl);
+        }
+      };
+
+      $('> span > a._jsonform-array-addmore', $nodeid).click(function (evt) {
+        evt.preventDefault();
+        evt.stopPropagation();
+        var idx = node.children.length;
+        if (boundaries.maxItems >= 0) {
+          if (node.children.length > boundaries.maxItems - 2) {
+            $nodeid.find('> span > a._jsonform-array-addmore')
+              .addClass('disabled');
+          }
+          if (node.children.length > boundaries.maxItems - 1) {
+            return false;
+          }
+        }
+        node.insertArrayItem(idx, $('> ul', $nodeid).get(0));
+        if ((boundaries.minItems <= 0) ||
+            ((boundaries.minItems > 0) &&
+              (node.children.length > boundaries.minItems - 1))) {
+          $nodeid.find('> span > a._jsonform-array-deletelast')
+            .removeClass('disabled');
+        }
+      });
+
+      //Simulate Users click to setup the form with its minItems
+      var curItems = $('> ul > li', $nodeid).length;
+      if ((boundaries.minItems > 0) &&
+          (curItems < boundaries.minItems)) {
+        for (var i = 0; i < (boundaries.minItems - 1) && ($nodeid.find('> ul > li').length < boundaries.minItems); i++) {
+          //console.log('Calling click: ',$nodeid);
+          //$('> span > a._jsonform-array-addmore', $nodeid).click();
+          node.insertArrayItem(curItems, $nodeid.find('> ul').get(0));
+        }
+      }
+      if ((boundaries.minItems > 0) &&
+          (node.children.length <= boundaries.minItems)) {
+        $nodeid.find('> span > a._jsonform-array-deletelast')
+          .addClass('disabled');
+      }
+
+      $('> span > a._jsonform-array-deletelast', $nodeid).click(function (evt) {
+        var idx = node.children.length - 1;
+        evt.preventDefault();
+        evt.stopPropagation();
+        if (boundaries.minItems > 0) {
+          if (node.children.length < boundaries.minItems + 2) {
+            $nodeid.find('> span > a._jsonform-array-deletelast')
+              .addClass('disabled');
+          }
+          if (node.children.length <= boundaries.minItems) {
+            return false;
+          }
+        }
+        else if (node.children.length === 1) {
+          $nodeid.find('> span > a._jsonform-array-deletelast')
+            .addClass('disabled');
+        }
+        node.deleteArrayItem(idx);
+        if ((boundaries.maxItems >= 0) && (idx <= boundaries.maxItems - 1)) {
+          $nodeid.find('> span > a._jsonform-array-addmore')
+            .removeClass('disabled');
+        }
+      });
+
+      if ($(node.el).sortable) {
+        $('> ul', $nodeid).sortable();
+        $('> ul', $nodeid).bind('sortstop', function (event, ui) {
+          var idx = $(ui.item).data('idx');
+          var newIdx = $(ui.item).index();
+          moveNodeTo(idx, newIdx);
+        });
+      }
+    }
+  },
+  'tabarray': {
+    'template': '<div id="<%= id %>"><div class="tabbable tabs-left">' +
+      '<ul class="nav nav-tabs">' +
+        '<%= tabs %>' +
+      '</ul>' +
+      '<div class="tab-content">' +
+        '<%= children %>' +
+      '</div>' +
+      '</div>' +
+      '<a href="#" class="btn btn-default _jsonform-array-addmore"><i class="glyphicon glyphicon-plus-sign" title="Add new"></i></a> ' +
+      '<a href="#" class="btn btn-default _jsonform-array-deleteitem"><i class="glyphicon glyphicon-minus-sign" title="Delete item"></i></a></div>',
+    'fieldtemplate': true,
+    'array': true,
+    'childTemplate': function (inner) {
+      return '<div data-idx="<%= node.childPos %>" class="tab-pane">' +
+        inner +
+        '</div>';
+    },
+    'onBeforeRender': function (data, node) {
+      // Generate the initial 'tabs' from the children
+      var tabs = '';
+      _.each(node.children, function (child, idx) {
+        var title = child.legend ||
+          child.title ||
+          ('Item ' + (idx+1));
+        tabs += '<li data-idx="' + idx + '"' +
+          ((idx === 0) ? ' class="active"' : '') +
+          '><a class="draggable tab" data-toggle="tab">' +
+          escapeHTML(title) +
+          '</a></li>';
+      });
+      data.tabs = tabs;
+    },
+    'onInsert': function (evt, node) {
+      var $nodeid = $(node.el).find('#' + escapeSelector(node.id));
+      var boundaries = node.getArrayBoundaries();
+
+      var moveNodeTo = function (fromIdx, toIdx) {
+        // Note "switchValuesWith" extracts values from the DOM since field
+        // values are not synchronized with the tree data structure, so calls
+        // to render are needed at each step to force values down to the DOM
+        // before next move.
+        // TODO: synchronize field values and data structure completely and
+        // call render only once to improve efficiency.
+        if (fromIdx === toIdx) return;
+        var incr = (fromIdx < toIdx) ? 1: -1;
+        var i = 0;
+        var tabEl = $('> .tabbable > .tab-content', $nodeid).get(0);
+        for (i = fromIdx; i !== toIdx; i += incr) {
+          node.children[i].switchValuesWith(node.children[i + incr]);
+          node.children[i].render(tabEl);
+          node.children[i + incr].render(tabEl);
+        }
+      };
+
+
+      // Refreshes the list of tabs
+      var updateTabs = function (selIdx) {
+        var tabs = '';
+        var activateFirstTab = false;
+        if (selIdx === undefined) {
+          selIdx = $('> .tabbable > .nav-tabs .active', $nodeid).data('idx');
+          if (selIdx) {
+            selIdx = parseInt(selIdx, 10);
+          }
+          else {
+            activateFirstTab = true;
+            selIdx = 0;
+          }
+        }
+        if (selIdx >= node.children.length) {
+          selIdx = node.children.length - 1;
+        }
+        _.each(node.children, function (child, idx) {
+          $('> .tabbable > .tab-content > [data-idx="' + idx + '"] > fieldset > legend', $nodeid).html(child.legend);
+          var title = child.legend || child.title || ('Item ' + (idx+1));
+          tabs += '<li data-idx="' + idx + '">' +
+                  '<a class="draggable tab" data-toggle="tab">' +
+                  escapeHTML(title) +
+                  '</a></li>';
+        });
+        $('> .tabbable > .nav-tabs', $nodeid).html(tabs);
+        if (activateFirstTab) {
+          $('> .tabbable > .nav-tabs [data-idx="0"]', $nodeid).addClass('active');
+        }
+        $('> .tabbable > .nav-tabs [data-toggle="tab"]', $nodeid).eq(selIdx).click();
+      };
+
+      $('> a._jsonform-array-deleteitem', $nodeid).click(function (evt) {
+        var idx = $('> .tabbable > .nav-tabs .active', $nodeid).data('idx');
+        evt.preventDefault();
+        evt.stopPropagation();
+        if (boundaries.minItems > 0) {
+          if (node.children.length < boundaries.minItems + 1) {
+            $nodeid.find('> a._jsonform-array-deleteitem')
+              .addClass('disabled');
+          }
+          if (node.children.length <= boundaries.minItems) return false;
+        }
+        node.deleteArrayItem(idx);
+        updateTabs();
+        if ((node.children.length < boundaries.minItems + 1) ||
+            (node.children.length === 0)) {
+          $nodeid.find('> a._jsonform-array-deleteitem').addClass('disabled');
+        }
+        if ((boundaries.maxItems >= 0) &&
+            (node.children.length <= boundaries.maxItems)) {
+          $nodeid.find('> a._jsonform-array-addmore').removeClass('disabled');
+        }
+      });
+
+      $('> a._jsonform-array-addmore', $nodeid).click(function (evt) {
+        var idx = node.children.length;
+        if (boundaries.maxItems>=0) {
+          if (node.children.length>boundaries.maxItems-2) {
+            $('> a._jsonform-array-addmore', $nodeid).addClass("disabled");
+          }
+          if (node.children.length > boundaries.maxItems - 1) {
+            return false;
+          }
+        }
+        evt.preventDefault();
+        evt.stopPropagation();
+        node.insertArrayItem(idx,
+          $nodeid.find('> .tabbable > .tab-content').get(0));
+        updateTabs(idx);
+        if ((boundaries.minItems <= 0) ||
+            ((boundaries.minItems > 0) && (idx > boundaries.minItems - 1))) {
+          $nodeid.find('> a._jsonform-array-deleteitem').removeClass('disabled');
+        }
+      });
+
+      $(node.el).on('legendUpdated', function (evt) {
+        updateTabs();
+        evt.preventDefault();
+        evt.stopPropagation();
+      });
+
+      if ($(node.el).sortable) {
+        $('> .tabbable > .nav-tabs', $nodeid).sortable({
+          containment: node.el,
+          tolerance: 'pointer'
+        });
+        $('> .tabbable > .nav-tabs', $nodeid).bind('sortstop', function (event, ui) {
+          var idx = $(ui.item).data('idx');
+          var newIdx = $(ui.item).index();
+          moveNodeTo(idx, newIdx);
+          updateTabs(newIdx);
+        });
+      }
+
+      // Simulate User's click to setup the form with its minItems
+      if ((boundaries.minItems >= 0)  && 
+          (node.children.length <= boundaries.minItems)) {
+        for (var i = 0; i < (boundaries.minItems - 1); i++) {
+          $nodeid.find('> a._jsonform-array-addmore').click();
+        }
+        $nodeid.find('> a._jsonform-array-deleteitem').addClass('disabled');
+        updateTabs();
+      }
+
+      if ((boundaries.maxItems >= 0) &&
+          (node.children.length >= boundaries.maxItems)) {
+        $nodeid.find('> a._jsonform-array-addmore').addClass('disabled');
+      }
+      if ((boundaries.minItems >= 0) &&
+          (node.children.length <= boundaries.minItems)) {
+        $nodeid.find('> a._jsonform-array-deleteitem').addClass('disabled');
+      }
+    }
+  },
+  'help': {
+    'template':'<span class="help-block" style="padding-top:5px"><%= elt.helpvalue %></span>',
+    'fieldtemplate': true
+  },
+  'msg': {
+    'template': '<%= elt.msg %>'
+  },
+  'fieldset': {
+    'template': '<fieldset class="form-group jsonform-error-<%= keydash %> <% if (elt.expandable) { %>expandable<% } %> <%= elt.htmlClass?elt.htmlClass:"" %>" ' +
+      '<% if (id) { %> id="<%= id %>"<% } %>' +
+      '>' +
+      '<% if (node.title || node.legend) { %><legend><%= node.title || node.legend %></legend><% } %>' +
+      '<% if (elt.expandable) { %><div class="form-group"><% } %>' +
+      '<%= children %>' +
+      '<% if (elt.expandable) { %></div><% } %>' +
+      '</fieldset>',
+    onInsert: function (evt, node) {
+      $('.expandable > div, .expandable > fieldset', node.el).hide();
+      // See #233 
+      $(".expandable", node.el).removeClass("expanded");
+    }
+  },
+  'advancedfieldset': {
+    'template': '<fieldset' +
+      '<% if (id) { %> id="<%= id %>"<% } %>' +
+      ' class="expandable <%= elt.htmlClass?elt.htmlClass:"" %>">' +
+      '<legend><%= (node.title || node.legend) ? (node.title || node.legend) : "Advanced options" %></legend>' +
+      '<div class="form-group">' +
+      '<%= children %>' +
+      '</div>' +
+      '</fieldset>',
+    onInsert: function (evt, node) {
+      $('.expandable > div, .expandable > fieldset', node.el).hide();
+      // See #233 
+      $(".expandable", node.el).removeClass("expanded");
+    }
+  },
+  'authfieldset': {
+    'template': '<fieldset' +
+      '<% if (id) { %> id="<%= id %>"<% } %>' +
+      ' class="expandable <%= elt.htmlClass?elt.htmlClass:"" %>">' +
+      '<legend><%= (node.title || node.legend) ? (node.title || node.legend) : "Authentication settings" %></legend>' +
+      '<div class="form-group">' +
+      '<%= children %>' +
+      '</div>' +
+      '</fieldset>',
+    onInsert: function (evt, node) {
+      $('.expandable > div, .expandable > fieldset', node.el).hide();
+      // See #233 
+      $(".expandable", node.el).removeClass("expanded");
+    }
+  },
+  'submit':{
+    'template':'<input type="submit" <% if (id) { %> id="<%= id %>" <% } %> class="btn btn-primary <%= elt.htmlClass?elt.htmlClass:"" %>" value="<%= value || node.title %>"<%= (node.disabled? " disabled" : "")%>/>'
+  },
+  'button':{
+    'template':' <button type="button" <% if (id) { %> id="<%= id %>" <% } %> class="btn btn-default <%= elt.htmlClass?elt.htmlClass:"" %>"><%= node.title %></button> '
+  },
+  'actions':{
+    'template':'<div class="<%= elt.htmlClass?elt.htmlClass:"" %>"><%= children %></div>'
+  },
+  'hidden':{
+    'template':'<input type="hidden" id="<%= id %>" name="<%= node.name %>" value="<%= escape(value) %>" />',
+    'inputfield': true
+  },
+  'selectfieldset': {
+    'template': '<fieldset class="tab-container <%= elt.htmlClass?elt.htmlClass:"" %>">' +
+      '<% if (node.legend) { %><legend><%= node.legend %></legend><% } %>' +
+      '<% if (node.formElement.key) { %><input type="hidden" id="<%= node.id %>" name="<%= node.name %>" value="<%= escape(value) %>" /><% } else { %>' +
+        '<a id="<%= node.id %>"></a><% } %>' +
+      '<div class="tabbable">' +
+        '<div class="form-group<%= node.formElement.hideMenu ? " hide" : "" %>">' +
+          '<% if (!elt.notitle) { %><label for="<%= node.id %>"><%= node.title ? node.title : node.name %></label><% } %>' +
+          '<div class="controls"><%= tabs %></div>' +
+        '</div>' +
+        '<div class="tab-content">' +
+          '<%= children %>' +
+        '</div>' +
+      '</div>' +
+      '</fieldset>',
+    'inputfield': true,
+    'getElement': function (el) {
+      return $(el).parent().get(0);
+    },
+    'childTemplate': function (inner) {
+      return '<div data-idx="<%= node.childPos %>" class="tab-pane' +
+        '<% if (node.active) { %> active<% } %>">' +
+        inner +
+        '</div>';
+    },
+    'onBeforeRender': function (data, node) {
+      // Before rendering, this function ensures that:
+      // 1. direct children have IDs (used to show/hide the tabs contents)
+      // 2. the tab to active is flagged accordingly. The active tab is
+      // the first one, except if form values are available, in which case
+      // it's the first tab for which there is some value available (or back
+      // to the first one if there are none)
+      // 3. the HTML of the select field used to select tabs is exposed in the
+      // HTML template data as "tabs"
+
+      var children = null;
+      var choices = [];
+      if (node.schemaElement) {
+        choices = node.schemaElement['enum'] || [];
+      }
+      if (node.options) {
+        children = _.map(node.options, function (option, idx) {
+          var child = node.children[idx];
+          child.childPos = idx; // When nested the childPos is always 0.
+          if (option instanceof Object) {
+            option = _.extend({ node: child }, option);
+            option.title = option.title ||
+              child.legend ||
+              child.title ||
+              ('Option ' + (child.childPos+1));
+            option.value = isSet(option.value) ? option.value :
+              isSet(choices[idx]) ? choices[idx] : idx;
+            return option;
+          }
+          else {
+            return {
+              title: option,
+              value: isSet(choices[child.childPos]) ?
+                choices[child.childPos] :
+                child.childPos,
+              node: child
+            };
+          }
+        });
+      }
+      else {
+        children = _.map(node.children, function (child, idx) {
+          child.childPos = idx; // When nested the childPos is always 0.
+          return {
+            title: child.legend || child.title || ('Option ' + (child.childPos+1)),
+            value: choices[child.childPos] || child.childPos,
+            node: child
+          };
+        });
+      }
+
+      // Reset each children to inactive so that they are not shown on insert
+      // The active one will then be shown later one. This is useful when sorting
+      // arrays with selectfieldset, otherwise both fields could be active at the
+      // same time.
+      _.each(children, function (child, idx) {
+        child.node.active = false
+      });
+
+      var activeChild = null;
+      if (data.value) {
+        activeChild = _.find(children, function (child) {
+          return (child.value === node.value);
+        });
+      }
+      if (!activeChild) {
+        activeChild = _.find(children, function (child) {
+          return child.node.hasNonDefaultValue();
+        });
+      }
+      if (!activeChild) {
+        activeChild = children[0];
+      }
+      activeChild.node.active = true;
+      data.value = activeChild.value;
+
+      var elt = node.formElement;
+      var tabs = '<select class="nav"' +
+        (node.disabled ? ' disabled' : '') +
+        '>';
+      _.each(children, function (child, idx) {
+        tabs += '<option data-idx="' + idx + '" value="' + child.value + '"' +
+          (child.node.active ? ' selected="selected" class="active"' : '') +
+          '>' +
+          escapeHTML(child.title) +
+          '</option>';
+      });
+      tabs += '</select>';
+
+      data.tabs = tabs;
+      return data;
+    },
+    'onInsert': function (evt, node) {
+      $(node.el).find('select.nav').first().on('change', function (evt) {
+        var $option = $(this).find('option:selected');
+        $(node.el).find('input[type="hidden"]').first().val($option.attr('value'));
+      });
+    }
+  },
+  'optionfieldset': {
+    'template': '<div' +
+      '<% if (node.id) { %> id="<%= node.id %>"<% } %>' +
+      '>' +
+      '<%= children %>' +
+      '</div>'
+  },
+  'section': {
+    'template': '<div' +
+      '<% if (elt.htmlClass) { %> class="<%= elt.htmlClass %>"<% } %>' +
+      '<% if (node.id) { %> id="<%= node.id %>"<% } %>' +
+      '><%= children %></div>'
+  },
+
+  /**
+   * A "questions" field renders a series of question fields and binds the
+   * result to the value of a schema key.
+   */
+  'questions': {
+    'template': '<div>' +
+      '<input type="hidden" id="<%= node.id %>" name="<%= node.name %>" value="<%= escape(value) %>" />' +
+      '<%= children %>' +
+      '</div>',
+    'fieldtemplate': true,
+    'inputfield': true,
+    'getElement': function (el) {
+      return $(el).parent().get(0);
+    },
+    'onInsert': function (evt, node) {
+      if (!node.children || (node.children.length === 0)) return;
+      _.each(node.children, function (child) {
+        $(child.el).hide();
+      });
+      $(node.children[0].el).show();
+    }
+  },
+
+  /**
+   * A "question" field lets user choose a response among possible choices.
+   * The field is not associated with any schema key. A question should be
+   * part of a "questions" field that binds a series of questions to a
+   * schema key.
+   */
+  'question': {
+    'template': '<div id="<%= node.id %>"><% _.each(node.options, function(key, val) { %><label class="<%= (node.formElement.optionsType === "radiobuttons") ? "btn btn-default" : "" %><%= ((key instanceof Object && key.htmlClass) ? " " + key.htmlClass : "") %>"><input type="radio" <% if (node.formElement.optionsType === "radiobuttons") { %> style="position:absolute;left:-9999px;" <% } %>name="<%= node.id %>" value="<%= val %>"<%= (node.disabled? " disabled" : "")%>/><span><%= (key instanceof Object ? key.title : key) %></span></label> <% }); %></div>',
+    'fieldtemplate': true,
+    'onInsert': function (evt, node) {
+      var activeClass = 'active';
+      var elt = node.formElement || {};
+      if (elt.activeClass) {
+        activeClass += ' ' + elt.activeClass;
+      }
+
+      // Bind to change events on radio buttons
+      $(node.el).find('input[type="radio"]').on('change', function (evt) {
+        var questionNode = null;
+        var option = node.options[$(this).val()];
+        if (!node.parentNode || !node.parentNode.el) return;
+
+        $(this).parent().parent().find('label').removeClass(activeClass);
+        $(this).parent().addClass(activeClass);
+        $(node.el).nextAll().hide();
+        $(node.el).nextAll().find('input[type="radio"]').prop('checked', false);
+
+        // Execute possible actions (set key value, form submission, open link,
+        // move on to next question)
+        if (option.value) {
+          // Set the key of the 'Questions' parent
+          $(node.parentNode.el).find('input[type="hidden"]').val(option.value);
+        }
+        if (option.next) {
+          questionNode = _.find(node.parentNode.children, function (child) {
+            return (child.formElement && (child.formElement.qid === option.next));
+          });
+          $(questionNode.el).show();
+          $(questionNode.el).nextAll().hide();
+          $(questionNode.el).nextAll().find('input[type="radio"]').prop('checked', false);
+        }
+        if (option.href) {
+          if (option.target) {
+            window.open(option.href, option.target);
+          }
+          else {
+            window.location = option.href;
+          }
+        }
+        if (option.submit) {
+          setTimeout(function () {
+            node.ownerTree.submit();
+          }, 0);
+        }
+      });
+    }
+  }
+};
+
+
+//Allow to access subproperties by splitting "."
+/**
+ * Retrieves the key identified by a path selector in the structured object.
+ *
+ * Levels in the path are separated by a dot. Array items are marked
+ * with [x]. For instance:
+ *  foo.bar[3].baz
+ *
+ * @function
+ * @param {Object} obj Structured object to parse
+ * @param {String} key Path to the key to retrieve
+ * @param {boolean} ignoreArrays True to use first element in an array when
+ *   stucked on a property. This parameter is basically only useful when
+ *   parsing a JSON schema for which the "items" property may either be an
+ *   object or an array with one object (only one because JSON form does not
+ *   support mix of items for arrays).
+ * @return {Object} The key's value.
+ */
+jsonform.util.getObjKey = function (obj, key, ignoreArrays) {
+  var innerobj = obj;
+  var keyparts = key.split(".");
+  var subkey = null;
+  var arrayMatch = null;
+  var prop = null;
+
+  for (var i = 0; i < keyparts.length; i++) {
+    if ((innerobj === null) || (typeof innerobj !== "object")) return null;
+    subkey = keyparts[i];
+    prop = subkey.replace(reArray, '');
+    reArray.lastIndex = 0;
+    arrayMatch = reArray.exec(subkey);
+    if (arrayMatch) {
+      while (true) {
+        if (prop && !_.isArray(innerobj[prop])) return null;
+        innerobj = prop ? innerobj[prop][parseInt(arrayMatch[1])] : innerobj[parseInt(arrayMatch[1])];
+        arrayMatch = reArray.exec(subkey);
+        if (!arrayMatch) break;
+        // In the case of multidimensional arrays,
+        // we should not take innerobj[prop][0] anymore,
+        // but innerobj[0] directly
+        prop = null;
+      }
+    } else if (ignoreArrays &&
+        !innerobj[prop] &&
+        _.isArray(innerobj) &&
+        innerobj[0]) {
+      innerobj = innerobj[0][prop];
+    } else {
+      innerobj = innerobj[prop];
+    }
+  }
+
+  if (ignoreArrays && _.isArray(innerobj) && innerobj[0]) {
+    return innerobj[0];
+  } else {
+    return innerobj;
+  }
+};
+
+
+/**
+ * Sets the key identified by a path selector to the given value.
+ *
+ * Levels in the path are separated by a dot. Array items are marked
+ * with [x]. For instance:
+ *  foo.bar[3].baz
+ *
+ * The hierarchy is automatically created if it does not exist yet.
+ *
+ * @function
+ * @param {Object} obj The object to build
+ * @param {String} key The path to the key to set where each level
+ *  is separated by a dot, and array items are flagged with [x].
+ * @param {Object} value The value to set, may be of any type.
+ */
+jsonform.util.setObjKey = function(obj,key,value) {
+  var innerobj = obj;
+  var keyparts = key.split(".");
+  var subkey = null;
+  var arrayMatch = null;
+  var prop = null;
+
+  for (var i = 0; i < keyparts.length-1; i++) {
+    subkey = keyparts[i];
+    prop = subkey.replace(reArray, '');
+    reArray.lastIndex = 0;
+    arrayMatch = reArray.exec(subkey);
+    if (arrayMatch) {
+      // Subkey is part of an array
+      while (true) {
+        if (!_.isArray(innerobj[prop])) {
+          innerobj[prop] = [];
+        }
+        innerobj = innerobj[prop];
+        prop = parseInt(arrayMatch[1], 10);
+        arrayMatch = reArray.exec(subkey);
+        if (!arrayMatch) break;
+      }
+      if ((typeof innerobj[prop] !== 'object') ||
+        (innerobj[prop] === null)) {
+        innerobj[prop] = {};
+      }
+      innerobj = innerobj[prop];
+    }
+    else {
+      // "Normal" subkey
+      if ((typeof innerobj[prop] !== 'object') ||
+        (innerobj[prop] === null)) {
+        innerobj[prop] = {};
+      }
+      innerobj = innerobj[prop];
+    }
+  }
+
+  // Set the final value
+  subkey = keyparts[keyparts.length - 1];
+  prop = subkey.replace(reArray, '');
+  reArray.lastIndex = 0;
+  arrayMatch = reArray.exec(subkey);
+  if (arrayMatch) {
+    while (true) {
+      if (!_.isArray(innerobj[prop])) {
+        innerobj[prop] = [];
+      }
+      innerobj = innerobj[prop];
+      prop = parseInt(arrayMatch[1], 10);
+      arrayMatch = reArray.exec(subkey);
+      if (!arrayMatch) break;
+    }
+    innerobj[prop] = value;
+  }
+  else {
+    innerobj[prop] = value;
+  }
+};
+
+
+/**
+ * Retrieves the key definition from the given schema.
+ *
+ * The key is identified by the path that leads to the key in the
+ * structured object that the schema would generate. Each level is
+ * separated by a '.'. Array levels are marked with []. For instance:
+ *  foo.bar[].baz
+ * ... to retrieve the definition of the key at the following location
+ * in the JSON schema (using a dotted path notation):
+ *  foo.properties.bar.items.properties.baz
+ *
+ * @function
+ * @param {Object} schema The JSON schema to retrieve the key from
+ * @param {String} key The path to the key, each level being separated
+ *  by a dot and array items being flagged with [].
+ * @return {Object} The key definition in the schema, null if not found.
+ */
+var getSchemaKey = function(schema,key) {
+  var schemaKey = key
+    .replace(/\./g, '.properties.')
+    .replace(/\[[0-9]*\]/g, '.items');
+  var schemaDef = jsonform.util.getObjKey(schema, schemaKey, true);
+  if (schemaDef && schemaDef.$ref) {
+    throw new Error('JSONForm does not yet support schemas that use the ' +
+      '$ref keyword. See: https://github.com/joshfire/jsonform/issues/54');
+  }
+  return schemaDef;
+};
+
+
+/**
+ * Truncates the key path to the requested depth.
+ *
+ * For instance, if the key path is:
+ *  foo.bar[].baz.toto[].truc[].bidule
+ * and the requested depth is 1, the returned key will be:
+ *  foo.bar[].baz.toto
+ *
+ * Note the function includes the path up to the next depth level.
+ *
+ * @function
+ * @param {String} key The path to the key in the schema, each level being
+ *  separated by a dot and array items being flagged with [].
+ * @param {Number} depth The array depth
+ * @return {String} The path to the key truncated to the given depth.
+ */
+var truncateToArrayDepth = function (key, arrayDepth) {
+  var depth = 0;
+  var pos = 0;
+  if (!key) return null;
+
+  if (arrayDepth > 0) {
+    while (depth < arrayDepth) {
+      pos = key.indexOf('[]', pos);
+      if (pos === -1) {
+        // Key path is not "deep" enough, simply return the full key
+        return key;
+      }
+      pos = pos + 2;
+      depth += 1;
+    }
+  }
+
+  // Move one step further to the right without including the final []
+  pos = key.indexOf('[]', pos);
+  if (pos === -1) return key;
+  else return key.substring(0, pos);
+};
+
+/**
+ * Applies the array path to the key path.
+ *
+ * For instance, if the key path is:
+ *  foo.bar[].baz.toto[].truc[].bidule
+ * and the arrayPath [4, 2], the returned key will be:
+ *  foo.bar[4].baz.toto[2].truc[].bidule
+ *
+ * @function
+ * @param {String} key The path to the key in the schema, each level being
+ *  separated by a dot and array items being flagged with [].
+ * @param {Array(Number)} arrayPath The array path to apply, e.g. [4, 2]
+ * @return {String} The path to the key that matches the array path.
+ */
+var applyArrayPath = function (key, arrayPath) {
+  var depth = 0;
+  if (!key) return null;
+  if (!arrayPath || (arrayPath.length === 0)) return key;
+  var newKey = key.replace(reArray, function (str, p1) {
+    // Note this function gets called as many times as there are [x] in the ID,
+    // from left to right in the string. The goal is to replace the [x] with
+    // the appropriate index in the new array path, if defined.
+    var newIndex = str;
+    if (isSet(arrayPath[depth])) {
+      newIndex = '[' + arrayPath[depth] + ']';
+    }
+    depth += 1;
+    return newIndex;
+  });
+  return newKey;
+};
+
+
+/**
+ * Returns the initial value that a field identified by its key
+ * should take.
+ *
+ * The "initial" value is defined as:
+ * 1. the previously submitted value if already submitted
+ * 2. the default value defined in the layout of the form
+ * 3. the default value defined in the schema
+ *
+ * The "value" returned is intended for rendering purpose,
+ * meaning that, for fields that define a titleMap property,
+ * the function returns the label, and not the intrinsic value.
+ *
+ * The function handles values that contains template strings,
+ * e.g. {{values.foo[].bar}} or {{idx}}.
+ *
+ * When the form is a string, the function truncates the resulting string
+ * to meet a potential "maxLength" constraint defined in the schema, using
+ * "..." to mark the truncation. Note it does not validate the resulting
+ * string against other constraints (e.g. minLength, pattern) as it would
+ * be hard to come up with an automated course of action to "fix" the value.
+ *
+ * @function
+ * @param {Object} formObject The JSON Form object
+ * @param {String} key The generic key path (e.g. foo[].bar.baz[])
+ * @param {Array(Number)} arrayPath The array path that identifies
+ *  the unique value in the submitted form (e.g. [1, 3])
+ * @param {Object} tpldata Template data object
+ * @param {Boolean} usePreviousValues true to use previously submitted values
+ *  if defined.
+ */
+var getInitialValue = function (formObject, key, arrayPath, tpldata, usePreviousValues) {
+  var value = null;
+
+  // Complete template data for template function
+  tpldata = tpldata || {};
+  tpldata.idx = tpldata.idx ||
+    (arrayPath ? arrayPath[arrayPath.length-1] : 1);
+  tpldata.value = isSet(tpldata.value) ? tpldata.value : '';
+  tpldata.getValue = tpldata.getValue || function (key) {
+    return getInitialValue(formObject, key, arrayPath, tpldata, usePreviousValues);
+  };
+
+  // Helper function that returns the form element that explicitly
+  // references the given key in the schema.
+  var getFormElement = function (elements, key) {
+    var formElement = null;
+    if (!elements || !elements.length) return null;
+    _.each(elements, function (elt) {
+      if (formElement) return;
+      if (elt === key) {
+        formElement = { key: elt };
+        return;
+      }
+      if (_.isString(elt)) return;
+      if (elt.key === key) {
+        formElement = elt;
+      }
+      else if (elt.items) {
+        formElement = getFormElement(elt.items, key);
+      }
+    });
+    return formElement;
+  };
+  var formElement = getFormElement(formObject.form || [], key);
+  var schemaElement = getSchemaKey(formObject.schema.properties, key);
+
+  if (usePreviousValues && formObject.value) {
+    // If values were previously submitted, use them directly if defined
+    value = jsonform.util.getObjKey(formObject.value, applyArrayPath(key, arrayPath));
+  }
+  if (!isSet(value)) {
+    if (formElement && (typeof formElement['value'] !== 'undefined')) {
+      // Extract the definition of the form field associated with
+      // the key as it may override the schema's default value
+      // (note a "null" value overrides a schema default value as well)
+      value = formElement['value'];
+    }
+    else if (schemaElement) {
+      // Simply extract the default value from the schema
+      if (isSet(schemaElement['default'])) {
+        value = schemaElement['default'];
+      }
+    }
+    if (value && value.indexOf('{{values.') !== -1) {
+      // This label wants to use the value of another input field.
+      // Convert that construct into {{getValue(key)}} for
+      // Underscore to call the appropriate function of formData
+      // when template gets called (note calling a function is not
+      // exactly Mustache-friendly but is supported by Underscore).
+      value = value.replace(
+        /\{\{values\.([^\}]+)\}\}/g,
+        '{{getValue("$1")}}');
+    }
+    if (value) {
+      value = _.template(value, valueTemplateSettings)(tpldata);
+    }
+  }
+
+  // TODO: handle on the formElement.options, because user can setup it too.
+  // Apply titleMap if needed
+  if (isSet(value) && formElement && hasOwnProperty(formElement.titleMap, value)) {
+    value = _.template(formElement.titleMap[value], valueTemplateSettings)(tpldata);
+  }
+
+  // Check maximum length of a string
+  if (value && _.isString(value) &&
+    schemaElement && schemaElement.maxLength) {
+    if (value.length > schemaElement.maxLength) {
+      // Truncate value to maximum length, adding continuation dots
+      value = value.substr(0, schemaElement.maxLength - 1) + '…';
+    }
+  }
+
+  if (!isSet(value)) {
+    return null;
+  }
+  else {
+    return value;
+  }
+};
+
+
+/**
+ * Represents a node in the form.
+ *
+ * Nodes that have an ID are linked to the corresponding DOM element
+ * when rendered
+ *
+ * Note the form element and the schema elements that gave birth to the
+ * node may be shared among multiple nodes (in the case of arrays).
+ *
+ * @class
+ */
+var formNode = function () {
+  /**
+   * The node's ID (may not be set)
+   */
+  this.id = null;
+
+  /**
+   * The node's key path (may not be set)
+   */
+  this.key = null;
+
+  /**
+   * DOM element associated witht the form element.
+   *
+   * The DOM element is set when the form element is rendered.
+   */
+  this.el = null;
+
+  /**
+   * Link to the form element that describes the node's layout
+   * (note the form element is shared among nodes in arrays)
+   */
+  this.formElement = null;
+
+  /**
+   * Link to the schema element that describes the node's value constraints
+   * (note the schema element is shared among nodes in arrays)
+   */
+  this.schemaElement = null;
+
+  /**
+   * Pointer to the "view" associated with the node, typically the right
+   * object in jsonform.elementTypes
+   */
+  this.view = null;
+
+  /**
+   * Node's subtree (if one is defined)
+   */
+  this.children = [];
+
+  /**
+   * A pointer to the form tree the node is attached to
+   */
+  this.ownerTree = null;
+
+  /**
+   * A pointer to the parent node of the node in the tree
+   */
+  this.parentNode = null;
+
+  /**
+   * Child template for array-like nodes.
+   *
+   * The child template gets cloned to create new array items.
+   */
+  this.childTemplate = null;
+
+
+  /**
+   * Direct children of array-like containers may use the value of a
+   * specific input field in their subtree as legend. The link to the
+   * legend child is kept here and initialized in computeInitialValues
+   * when a child sets "valueInLegend"
+   */
+  this.legendChild = null;
+
+
+  /**
+   * The path of indexes that lead to the current node when the
+   * form element is not at the root array level.
+   *
+   * Note a form element may well be nested element and still be
+   * at the root array level. That's typically the case for "fieldset"
+   * elements. An array level only gets created when a form element
+   * is of type "array" (or a derivated type such as "tabarray").
+   *
+   * The array path of a form element linked to the foo[2].bar.baz[3].toto
+   * element in the submitted values is [2, 3] for instance.
+   *
+   * The array path is typically used to compute the right ID for input
+   * fields. It is also used to update positions when an array item is
+   * created, moved around or suppressed.
+   *
+   * @type {Array(Number)}
+   */
+  this.arrayPath = [];
+
+  /**
+   * Position of the node in the list of children of its parents
+   */
+  this.childPos = 0;
+};
+
+
+/**
+ * Clones a node
+ *
+ * @function
+ * @param {formNode} New parent node to attach the node to
+ * @return {formNode} Cloned node
+ */
+formNode.prototype.clone = function (parentNode) {
+  var node = new formNode();
+  node.arrayPath = _.clone(this.arrayPath);
+  node.ownerTree = this.ownerTree;
+  node.parentNode = parentNode || this.parentNode;
+  node.formElement = this.formElement;
+  node.schemaElement = this.schemaElement;
+  node.view = this.view;
+  node.children = _.map(this.children, function (child) {
+    return child.clone(node);
+  });
+  if (this.childTemplate) {
+    node.childTemplate = this.childTemplate.clone(node);
+  }
+  return node;
+};
+
+
+/**
+ * Returns true if the subtree that starts at the current node
+ * has some non empty value attached to it
+ */
+formNode.prototype.hasNonDefaultValue = function () {
+
+  // hidden elements don't count because they could make the wrong selectfieldset element active
+  if (this.formElement && this.formElement.type=="hidden") {
+    return false;
+  }
+
+  if (this.value && !this.defaultValue) {
+    return true;
+  }
+  var child = _.find(this.children, function (child) {
+    return child.hasNonDefaultValue();
+  });
+  return !!child;
+};
+
+
+/**
+ * Attaches a child node to the current node.
+ *
+ * The child node is appended to the end of the list.
+ *
+ * @function
+ * @param {formNode} node The child node to append
+ * @return {formNode} The inserted node (same as the one given as parameter)
+ */
+formNode.prototype.appendChild = function (node) {
+  node.parentNode = this;
+  node.childPos = this.children.length;
+  this.children.push(node);
+  return node;
+};
+
+
+/**
+ * Removes the last child of the node.
+ *
+ * @function
+ */
+formNode.prototype.removeChild = function () {
+  var child = this.children[this.children.length-1];
+  if (!child) return;
+
+  // Remove the child from the DOM
+  $(child.el).remove();
+
+  // Remove the child from the array
+  return this.children.pop();
+};
+
+
+/**
+ * Moves the user entered values set in the current node's subtree to the
+ * given node's subtree.
+ *
+ * The target node must follow the same structure as the current node
+ * (typically, they should have been generated from the same node template)
+ *
+ * The current node MUST be rendered in the DOM.
+ *
+ * TODO: when current node is not in the DOM, extract values from formNode.value
+ * properties, so that the function be available even when current node is not
+ * in the DOM.
+ *
+ * Moving values around allows to insert/remove array items at arbitrary
+ * positions.
+ *
+ * @function
+ * @param {formNode} node Target node.
+ */
+formNode.prototype.moveValuesTo = function (node) {
+  var values = this.getFormValues(node.arrayPath);
+  node.resetValues();
+  node.computeInitialValues(values, true);
+};
+
+
+/**
+ * Switches nodes user entered values.
+ *
+ * The target node must follow the same structure as the current node
+ * (typically, they should have been generated from the same node template)
+ *
+ * Both nodes MUST be rendered in the DOM.
+ *
+ * TODO: update getFormValues to work even if node is not rendered, using
+ * formNode's "value" property.
+ *
+ * @function
+ * @param {formNode} node Target node
+ */
+formNode.prototype.switchValuesWith = function (node) {
+  var values = this.getFormValues(node.arrayPath);
+  var nodeValues = node.getFormValues(this.arrayPath);
+  node.resetValues();
+  node.computeInitialValues(values, true);
+  this.resetValues();
+  this.computeInitialValues(nodeValues, true);
+};
+
+
+/**
+ * Resets all DOM values in the node's subtree.
+ *
+ * This operation also drops all array item nodes.
+ * Note values are not reset to their default values, they are rather removed!
+ *
+ * @function
+ */
+formNode.prototype.resetValues = function () {
+  var params = null;
+  var idx = 0;
+
+  // Reset value
+  this.value = null;
+
+  // Propagate the array path from the parent node
+  // (adding the position of the child for nodes that are direct
+  // children of array-like nodes)
+  if (this.parentNode) {
+    this.arrayPath = _.clone(this.parentNode.arrayPath);
+    if (this.parentNode.view && this.parentNode.view.array) {
+      this.arrayPath.push(this.childPos);
+    }
+  }
+  else {
+    this.arrayPath = [];
+  }
+
+  if (this.view && this.view.inputfield) {
+    // Simple input field, extract the value from the origin,
+    // set the target value and reset the origin value
+    params = $(':input', this.el).serializeArray();
+    _.each(params, function (param) {
+      // TODO: check this, there may exist corner cases with this approach
+      // (with multiple checkboxes for instance)
+      $('[name="' + escapeSelector(param.name) + '"]', $(this.el)).val('');
+    }, this);
+  }
+  else if (this.view && this.view.array) {
+    // The current node is an array, drop all children
+    while (this.children.length > 0) {
+      this.removeChild();
+    }
+  }
+
+  // Recurse down the tree
+  _.each(this.children, function (child) {
+    child.resetValues();
+  });
+};
+
+
+/**
+ * Sets the child template node for the current node.
+ *
+ * The child template node is used to create additional children
+ * in an array-like form element. The template is never rendered.
+ *
+ * @function
+ * @param {formNode} node The child template node to set
+ */
+formNode.prototype.setChildTemplate = function (node) {
+  this.childTemplate = node;
+  node.parentNode = this;
+};
+
+
+/**
+ * Recursively sets values to all nodes of the current subtree
+ * based on previously submitted values, or based on default
+ * values when the submitted values are not enough
+ *
+ * The function should be called once in the lifetime of a node
+ * in the tree. It expects its parent's arrayPath to be up to date.
+ *
+ * Three cases may arise:
+ * 1. if the form element is a simple input field, the value is
+ * extracted from previously submitted values of from default values
+ * defined in the schema.
+ * 2. if the form element is an array-like node, the child template
+ * is used to create as many children as possible (and at least one).
+ * 3. the function simply recurses down the node's subtree otherwise
+ * (this happens when the form element is a fieldset-like element).
+ *
+ * @function
+ * @param {Object} values Previously submitted values for the form
+ * @param {Boolean} ignoreDefaultValues Ignore default values defined in the
+ *  schema when set.
+ */
+formNode.prototype.computeInitialValues = function (values, ignoreDefaultValues) {
+  var self = this;
+  var node = null;
+  var nbChildren = 1;
+  var i = 0;
+  var formData = this.ownerTree.formDesc.tpldata || {};
+
+  // Propagate the array path from the parent node
+  // (adding the position of the child for nodes that are direct
+  // children of array-like nodes)
+  if (this.parentNode) {
+    this.arrayPath = _.clone(this.parentNode.arrayPath);
+    if (this.parentNode.view && this.parentNode.view.array) {
+      this.arrayPath.push(this.childPos);
+    }
+  }
+  else {
+    this.arrayPath = [];
+  }
+
+  // Prepare special data param "idx" for templated values
+  // (is is the index of the child in its wrapping array, starting
+  // at 1 since that's more human-friendly than a zero-based index)
+  formData.idx = (this.arrayPath.length > 0) ?
+    this.arrayPath[this.arrayPath.length-1] + 1 :
+    this.childPos + 1;
+
+  // Prepare special data param "value" for templated values
+  formData.value = '';
+
+  // Prepare special function to compute the value of another field
+  formData.getValue = function (key) {
+    if (!values) {
+      return '';
+    }
+    var returnValue = values;
+    var listKey = key.split('[].');
+    var i;
+    for (i = 0; i < listKey.length - 1; i++) {
+      returnValue = returnValue[listKey[i]][self.arrayPath[i]];
+    }
+    return returnValue[listKey[i]];
+  };
+
+  if (this.formElement) {
+    // Compute the ID of the field (if needed)
+    if (this.formElement.id) {
+      this.id = applyArrayPath(this.formElement.id, this.arrayPath);
+    }
+    else if (this.view && this.view.array) {
+      this.id = escapeSelector(this.ownerTree.formDesc.prefix) +
+        '-elt-counter-' + _.uniqueId();
+    }
+    else if (this.parentNode && this.parentNode.view &&
+      this.parentNode.view.array) {
+      // Array items need an array to associate the right DOM element
+      // to the form node when the parent is rendered.
+      this.id = escapeSelector(this.ownerTree.formDesc.prefix) +
+        '-elt-counter-' + _.uniqueId();
+    }
+    else if ((this.formElement.type === 'button') ||
+      (this.formElement.type === 'selectfieldset') ||
+      (this.formElement.type === 'question') ||
+      (this.formElement.type === 'buttonquestion')) {
+      // Buttons do need an id for "onClick" purpose
+      this.id = escapeSelector(this.ownerTree.formDesc.prefix) +
+        '-elt-counter-' + _.uniqueId();
+    }
+
+    // Compute the actual key (the form element's key is index-free,
+    // i.e. it looks like foo[].bar.baz[].truc, so we need to apply
+    // the array path of the node to get foo[4].bar.baz[2].truc)
+    if (this.formElement.key) {
+      this.key = applyArrayPath(this.formElement.key, this.arrayPath);
+      this.keydash = slugify(this.key.replace(/\./g, '---'));
+    }
+
+    // Same idea for the field's name
+    this.name = applyArrayPath(this.formElement.name, this.arrayPath);
+
+    // Consider that label values are template values and apply the
+    // form's data appropriately (note we also apply the array path
+    // although that probably doesn't make much sense for labels...)
+    _.each([
+      'title',
+      'legend',
+      'description',
+      'append',
+      'prepend',
+      'inlinetitle',
+      'helpvalue',
+      'value',
+      'disabled',
+      'placeholder',
+      'readOnly'
+    ], function (prop) {
+      if (_.isString(this.formElement[prop])) {
+        if (this.formElement[prop].indexOf('{{values.') !== -1) {
+          // This label wants to use the value of another input field.
+          // Convert that construct into {{jsonform.getValue(key)}} for
+          // Underscore to call the appropriate function of formData
+          // when template gets called (note calling a function is not
+          // exactly Mustache-friendly but is supported by Underscore).
+          this[prop] = this.formElement[prop].replace(
+            /\{\{values\.([^\}]+)\}\}/g,
+            '{{getValue("$1")}}');
+        }
+        else {
+          // Note applying the array path probably doesn't make any sense,
+          // but some geek might want to have a label "foo[].bar[].baz",
+          // with the [] replaced by the appropriate array path.
+          this[prop] = applyArrayPath(this.formElement[prop], this.arrayPath);
+        }
+        if (this[prop]) {
+          this[prop] = _.template(this[prop], valueTemplateSettings)(formData);
+        }
+      }
+      else {
+        this[prop] = this.formElement[prop];
+      }
+    }, this);
+
+    // Apply templating to options created with "titleMap" as well
+    if (this.formElement.options) {
+      this.options = _.map(this.formElement.options, function (option) {
+        var title = null;
+        if (_.isObject(option) && option.title) {
+          // See a few lines above for more details about templating
+          // preparation here.
+          if (option.title.indexOf('{{values.') !== -1) {
+            title = option.title.replace(
+              /\{\{values\.([^\}]+)\}\}/g,
+              '{{getValue("$1")}}');
+          }
+          else {
+            title = applyArrayPath(option.title, self.arrayPath);
+          }
+          return _.extend({}, option, {
+            value: (isSet(option.value) ? option.value : ''),
+            title: _.template(title, valueTemplateSettings)(formData)
+          });
+        }
+        else {
+          return option;
+        }
+      });
+    }
+  }
+
+  if (this.view && this.view.inputfield && this.schemaElement) {
+    // Case 1: simple input field
+    if (values) {
+      // Form has already been submitted, use former value if defined.
+      // Note we won't set the field to its default value otherwise
+      // (since the user has already rejected it)
+      if (isSet(jsonform.util.getObjKey(values, this.key))) {
+        this.value = jsonform.util.getObjKey(values, this.key);
+      }
+    }
+    else if (!ignoreDefaultValues) {
+      // No previously submitted form result, use default value
+      // defined in the schema if it's available and not already
+      // defined in the form element
+      if (!isSet(this.value) && isSet(this.schemaElement['default'])) {
+        this.value = this.schemaElement['default'];
+        if (_.isString(this.value)) {
+          if (this.value.indexOf('{{values.') !== -1) {
+            // This label wants to use the value of another input field.
+            // Convert that construct into {{jsonform.getValue(key)}} for
+            // Underscore to call the appropriate function of formData
+            // when template gets called (note calling a function is not
+            // exactly Mustache-friendly but is supported by Underscore).
+            this.value = this.value.replace(
+              /\{\{values\.([^\}]+)\}\}/g,
+              '{{getValue("$1")}}');
+          }
+          else {
+            // Note applying the array path probably doesn't make any sense,
+            // but some geek might want to have a label "foo[].bar[].baz",
+            // with the [] replaced by the appropriate array path.
+            this.value = applyArrayPath(this.value, this.arrayPath);
+          }
+          if (this.value) {
+            this.value = _.template(this.value, valueTemplateSettings)(formData);
+          }
+        }
+        this.defaultValue = true;
+      }
+    }
+  }
+  else if (this.view && this.view.array) {
+    // Case 2: array-like node
+    nbChildren = 0;
+    if (values) {
+      nbChildren = this.getPreviousNumberOfItems(values, this.arrayPath);
+    }
+    // TODO: use default values at the array level when form has not been
+    // submitted before. Note it's not that easy because each value may
+    // be a complex structure that needs to be pushed down the subtree.
+    // The easiest way is probably to generate a "values" object and
+    // compute initial values from that object
+    /*
+    else if (this.schemaElement['default']) {
+      nbChildren = this.schemaElement['default'].length;
+    }
+    */
+    else if (nbChildren === 0) {
+      // If form has already been submitted with no children, the array
+      // needs to be rendered without children. If there are no previously
+      // submitted values, the array gets rendered with one empty item as
+      // it's more natural from a user experience perspective. That item can
+      // be removed with a click on the "-" button.
+      nbChildren = 1;
+    }
+    for (i = 0; i < nbChildren; i++) {
+      this.appendChild(this.childTemplate.clone());
+    }
+  }
+
+  // Case 3 and in any case: recurse through the list of children
+  _.each(this.children, function (child) {
+    child.computeInitialValues(values, ignoreDefaultValues);
+  });
+
+  // If the node's value is to be used as legend for its "container"
+  // (typically the array the node belongs to), ensure that the container
+  // has a direct link to the node for the corresponding tab.
+  if (this.formElement && this.formElement.valueInLegend) {
+    node = this;
+    while (node) {
+      if (node.parentNode &&
+        node.parentNode.view &&
+        node.parentNode.view.array) {
+        node.legendChild = this;
+        if (node.formElement && node.formElement.legend) {
+          node.legend = applyArrayPath(node.formElement.legend, node.arrayPath);
+          formData.idx = (node.arrayPath.length > 0) ?
+            node.arrayPath[node.arrayPath.length-1] + 1 :
+            node.childPos + 1;
+          formData.value = isSet(this.value) ? this.value : '';
+          node.legend = _.template(node.legend, valueTemplateSettings)(formData);
+          break;
+        }
+      }
+      node = node.parentNode;
+    }
+  }
+};
+
+
+/**
+ * Returns the number of items that the array node should have based on
+ * previously submitted values.
+ *
+ * The whole difficulty is that values may be hidden deep in the subtree
+ * of the node and may actually target different arrays in the JSON schema.
+ *
+ * @function
+ * @param {Object} values Previously submitted values
+ * @param {Array(Number)} arrayPath the array path we're interested in
+ * @return {Number} The number of items in the array
+ */
+formNode.prototype.getPreviousNumberOfItems = function (values, arrayPath) {
+  var key = null;
+  var arrayValue = null;
+  var childNumbers = null;
+  var idx = 0;
+
+  if (!values) {
+    // No previously submitted values, no need to go any further
+    return 0;
+  }
+
+  if (this.view.inputfield && this.schemaElement) {
+    // Case 1: node is a simple input field that links to a key in the schema.
+    // The schema key looks typically like:
+    //  foo.bar[].baz.toto[].truc[].bidule
+    // The goal is to apply the array path and truncate the key to the last
+    // array we're interested in, e.g. with an arrayPath [4, 2]:
+    //  foo.bar[4].baz.toto[2]
+    key = truncateToArrayDepth(this.formElement.key, arrayPath.length);
+    key = applyArrayPath(key, arrayPath);
+    arrayValue = jsonform.util.getObjKey(values, key);
+    if (!arrayValue) {
+      // No key? That means this field had been left empty
+      // in previous submit
+      return 0;
+    }
+    childNumbers = _.map(this.children, function (child) {
+      return child.getPreviousNumberOfItems(values, arrayPath);
+    });
+    return _.max([_.max(childNumbers) || 0, arrayValue.length]);
+  }
+  else if (this.view.array) {
+    // Case 2: node is an array-like node, look for input fields
+    // in its child template
+    return this.childTemplate.getPreviousNumberOfItems(values, arrayPath);
+  }
+  else {
+    // Case 3: node is a leaf or a container,
+    // recurse through the list of children and return the maximum
+    // number of items found in each subtree
+    childNumbers = _.map(this.children, function (child) {
+      return child.getPreviousNumberOfItems(values, arrayPath);
+    });
+    return _.max(childNumbers) || 0;
+  }
+};
+
+
+/**
+ * Returns the structured object that corresponds to the form values entered
+ * by the user for the node's subtree.
+ *
+ * The returned object follows the structure of the JSON schema that gave
+ * birth to the form.
+ *
+ * Obviously, the node must have been rendered before that function may
+ * be called.
+ *
+ * @function
+ * @param {Array(Number)} updateArrayPath Array path to use to pretend that
+ *  the entered values were actually entered for another item in an array
+ *  (this is used to move values around when an item is inserted/removed/moved
+ *  in an array)
+ * @return {Object} The object that follows the data schema and matches the
+ *  values entered by the user.
+ */
+formNode.prototype.getFormValues = function (updateArrayPath) {
+  // The values object that will be returned
+  var values = {};
+
+  if (!this.el) {
+    throw new Error('formNode.getFormValues can only be called on nodes that are associated with a DOM element in the tree');
+  }
+
+  // Form fields values
+  var formArray = $(':input', this.el).serializeArray();
+
+  // Set values to false for unset checkboxes and radio buttons
+  // because serializeArray() ignores them
+  formArray = formArray.concat(
+    $(':input[type=checkbox]:not(:disabled):not(:checked)', this.el).map( function() {
+      return {"name": this.name, "value": this.checked}
+    }).get()
+  );
+
+  if (updateArrayPath) {
+    _.each(formArray, function (param) {
+      param.name = applyArrayPath(param.name, updateArrayPath);
+    });
+  }
+
+  // The underlying data schema
+  var formSchema = this.ownerTree.formDesc.schema;
+
+  for (var i = 0; i < formArray.length; i++) {
+    // Retrieve the key definition from the data schema
+    var name = formArray[i].name;
+    var eltSchema = getSchemaKey(formSchema.properties, name);
+    var arrayMatch = null;
+    var cval = null;
+
+    // Skip the input field if it's not part of the schema
+    if (!eltSchema) continue;
+
+    // Handle multiple checkboxes separately as the idea is to generate
+    // an array that contains the list of enumeration items that the user
+    // selected.
+    if (eltSchema._jsonform_checkboxes_as_array) {
+      arrayMatch = name.match(/\[([0-9]*)\]$/);
+      if (arrayMatch) {
+        name = name.replace(/\[([0-9]*)\]$/, '');
+        cval = jsonform.util.getObjKey(values, name) || [];
+        if (formArray[i].value === '1') {
+          // Value selected, push the corresponding enumeration item
+          // to the data result
+          cval.push(eltSchema['enum'][parseInt(arrayMatch[1],10)]);
+        }
+        jsonform.util.setObjKey(values, name, cval);
+        continue;
+      }
+    }
+
+    // Type casting
+    if (eltSchema.type === 'boolean') {
+      if (formArray[i].value === '0') {
+        formArray[i].value = false;
+      } else {
+        formArray[i].value = !!formArray[i].value;
+      }
+    }
+    if ((eltSchema.type === 'number') ||
+      (eltSchema.type === 'integer')) {
+      if (_.isString(formArray[i].value)) {
+        if (!formArray[i].value.length) {
+          formArray[i].value = null;
+        } else if (!isNaN(Number(formArray[i].value))) {
+          formArray[i].value = Number(formArray[i].value);
+        }
+      }
+    }
+    if ((eltSchema.type === 'string') &&
+      (formArray[i].value === '') &&
+      !eltSchema._jsonform_allowEmpty) {
+      formArray[i].value=null;
+    }
+    if ((eltSchema.type === 'object') &&
+      _.isString(formArray[i].value) &&
+      (formArray[i].value.substring(0,1) === '{')) {
+      try {
+        formArray[i].value = JSON.parse(formArray[i].value);
+      } catch (e) {
+        formArray[i].value = {};
+      }
+    }
+    //TODO is this due to a serialization bug?
+    if ((eltSchema.type === 'object') &&
+      (formArray[i].value === 'null' || formArray[i].value === '')) {
+      formArray[i].value = null;
+    }
+
+    if (formArray[i].name && (formArray[i].value !== null)) {
+      jsonform.util.setObjKey(values, formArray[i].name, formArray[i].value);
+    }
+  }
+  // console.log("Form value",values);
+  return values;
+};
+
+
+
+/**
+ * Renders the node.
+ *
+ * Rendering is done in three steps: HTML generation, DOM element creation
+ * and insertion, and an enhance step to bind event handlers.
+ *
+ * @function
+ * @param {Node} el The DOM element where the node is to be rendered. The
+ *  node is inserted at the right position based on its "childPos" property.
+ */
+formNode.prototype.render = function (el) {
+  var html = this.generate();
+  this.setContent(html, el);
+  this.enhance();
+};
+
+
+/**
+ * Inserts/Updates the HTML content of the node in the DOM.
+ *
+ * If the HTML is an update, the new HTML content replaces the old one.
+ * The new HTML content is not moved around in the DOM in particular.
+ *
+ * The HTML is inserted at the right position in its parent's DOM subtree
+ * otherwise (well, provided there are enough children, but that should always
+ * be the case).
+ *
+ * @function
+ * @param {string} html The HTML content to render
+ * @param {Node} parentEl The DOM element that is to contain the DOM node.
+ *  This parameter is optional (the node's parent is used otherwise) and
+ *  is ignored if the node to render is already in the DOM tree.
+ */
+formNode.prototype.setContent = function (html, parentEl) {
+  var node = $(html);
+  var parentNode = parentEl ||
+    (this.parentNode ? this.parentNode.el : this.ownerTree.domRoot);
+  var nextSibling = null;
+
+  if (this.el) {
+    // Replace the contents of the DOM element if the node is already in the tree
+    $(this.el).replaceWith(node);
+  }
+  else {
+    // Insert the node in the DOM if it's not already there
+    nextSibling = $(parentNode).children().get(this.childPos);
+    if (nextSibling) {
+      $(nextSibling).before(node);
+    }
+    else {
+      $(parentNode).append(node);
+    }
+  }
+
+  // Save the link between the form node and the generated HTML
+  this.el = node;
+
+  // Update the node's subtree, extracting DOM elements that match the nodes
+  // from the generated HTML
+  this.updateElement(this.el);
+};
+
+
+/**
+ * Updates the DOM element associated with the node.
+ *
+ * Only nodes that have ID are directly associated with a DOM element.
+ *
+ * @function
+ */
+formNode.prototype.updateElement = function (domNode) {
+  if (this.id) {
+    this.el = $('#' + escapeSelector(this.id), domNode).get(0);
+    if (this.view && this.view.getElement) {
+      this.el = this.view.getElement(this.el);
+    }
+    if ((this.fieldtemplate !== false) &&
+      this.view && this.view.fieldtemplate) {
+      // The field template wraps the element two or three level deep
+      // in the DOM tree, depending on whether there is anything prepended
+      // or appended to the input field
+      this.el = $(this.el).parent().parent();
+      if (this.prepend || this.prepend) {
+        this.el = this.el.parent();
+      }
+      this.el = this.el.get(0);
+    }
+    if (this.parentNode && this.parentNode.view &&
+      this.parentNode.view.childTemplate) {
+      // TODO: the child template may introduce more than one level,
+      // so the number of levels introduced should rather be exposed
+      // somehow in jsonform.fieldtemplate.
+      this.el = $(this.el).parent().get(0);
+    }
+  }
+
+  _.each(this.children, function (child) {
+    child.updateElement(this.el || domNode);
+  });
+};
+
+
+/**
+ * Generates the view's HTML content for the underlying model.
+ *
+ * @function
+ */
+formNode.prototype.generate = function () {
+  var data = {
+    id: this.id,
+    keydash: this.keydash,
+    elt: this.formElement,
+    schema: this.schemaElement,
+    node: this,
+    value: isSet(this.value) ? this.value : '',
+    escape: escapeHTML
+  };
+  var template = null;
+  var html = '';
+
+  // Complete the data context if needed
+  if (this.ownerTree.formDesc.onBeforeRender) {
+    this.ownerTree.formDesc.onBeforeRender(data, this);
+  }
+  if (this.view.onBeforeRender) {
+    this.view.onBeforeRender(data, this);
+  }
+
+  // Use the template that 'onBeforeRender' may have set,
+  // falling back to that of the form element otherwise
+  if (this.template) {
+    template = this.template;
+  }
+  else if (this.formElement && this.formElement.template) {
+    template = this.formElement.template;
+  }
+  else {
+    template = this.view.template;
+  }
+
+  // Wrap the view template in the generic field template
+  // (note the strict equality to 'false', needed as we fallback
+  // to the view's setting otherwise)
+  if ((this.fieldtemplate !== false) &&
+    (this.fieldtemplate || this.view.fieldtemplate)) {
+    template = jsonform.fieldTemplate(template);
+  }
+
+  // Wrap the content in the child template of its parent if necessary.
+  if (this.parentNode && this.parentNode.view &&
+    this.parentNode.view.childTemplate) {
+    template = this.parentNode.view.childTemplate(template);
+  }
+
+  // Prepare the HTML of the children
+  var childrenhtml = '';
+  _.each(this.children, function (child) {
+    childrenhtml += child.generate();
+  });
+  data.children = childrenhtml;
+
+  data.fieldHtmlClass = '';
+  if (this.ownerTree &&
+      this.ownerTree.formDesc &&
+      this.ownerTree.formDesc.params &&
+      this.ownerTree.formDesc.params.fieldHtmlClass) {
+    data.fieldHtmlClass = this.ownerTree.formDesc.params.fieldHtmlClass;
+  }
+  if (this.formElement &&
+      (typeof this.formElement.fieldHtmlClass !== 'undefined')) {
+    data.fieldHtmlClass = this.formElement.fieldHtmlClass;
+  }
+
+  // Apply the HTML template
+  html = _.template(template, fieldTemplateSettings)(data);
+  return html;
+};
+
+
+/**
+ * Enhances the view with additional logic, binding event handlers
+ * in particular.
+ *
+ * The function also runs the "insert" event handler of the view and
+ * form element if they exist (starting with that of the view)
+ *
+ * @function
+ */
+formNode.prototype.enhance = function () {
+  var node = this;
+  var handlers = null;
+  var handler = null;
+  var formData = _.clone(this.ownerTree.formDesc.tpldata) || {};
+
+  if (this.formElement) {
+    // Check the view associated with the node as it may define an "onInsert"
+    // event handler to be run right away
+    if (this.view.onInsert) {
+      this.view.onInsert({ target: $(this.el) }, this);
+    }
+
+    handlers = this.handlers || this.formElement.handlers;
+
+    // Trigger the "insert" event handler
+    handler = this.onInsert || this.formElement.onInsert;
+    if (handler) {
+      handler({ target: $(this.el) }, this);
+    }
+    if (handlers) {
+      _.each(handlers, function (handler, onevent) {
+        if (onevent === 'insert') {
+          handler({ target: $(this.el) }, this);
+        }
+      }, this);
+    }
+
+    // No way to register event handlers if the DOM element is unknown
+    // TODO: find some way to register event handlers even when this.el is not set.
+    if (this.el) {
+
+      // Register specific event handlers
+      // TODO: Add support for other event handlers
+      if (this.onChange)
+        $(this.el).bind('change', function(evt) { node.onChange(evt, node); });
+      if (this.view.onChange)
+        $(this.el).bind('change', function(evt) { node.view.onChange(evt, node); });
+      if (this.formElement.onChange)
+        $(this.el).bind('change', function(evt) { node.formElement.onChange(evt, node); });
+
+      if (this.onClick)
+        $(this.el).bind('click', function(evt) { node.onClick(evt, node); });
+      if (this.view.onClick)
+        $(this.el).bind('click', function(evt) { node.view.onClick(evt, node); });
+      if (this.formElement.onClick)
+        $(this.el).bind('click', function(evt) { node.formElement.onClick(evt, node); });
+
+      if (this.onKeyUp)
+        $(this.el).bind('keyup', function(evt) { node.onKeyUp(evt, node); });
+      if (this.view.onKeyUp)
+        $(this.el).bind('keyup', function(evt) { node.view.onKeyUp(evt, node); });
+      if (this.formElement.onKeyUp)
+        $(this.el).bind('keyup', function(evt) { node.formElement.onKeyUp(evt, node); });
+
+      if (handlers) {
+        _.each(handlers, function (handler, onevent) {
+          if (onevent !== 'insert') {
+            $(this.el).bind(onevent, function(evt) { handler(evt, node); });
+          }
+        }, this);
+      }
+    }
+
+    // Auto-update legend based on the input field that's associated with it
+    if (this.legendChild && this.legendChild.formElement) {
+      var onChangeHandler = function (evt) {
+        if (node.formElement && node.formElement.legend && node.parentNode) {
+          node.legend = applyArrayPath(node.formElement.legend, node.arrayPath);
+          formData.idx = (node.arrayPath.length > 0) ?
+              node.arrayPath[node.arrayPath.length - 1] + 1 :
+              node.childPos + 1;
+          formData.value = $(evt.target).val();
+          node.legend = _.template(node.legend, valueTemplateSettings)(formData);
+          $(node.parentNode.el).trigger('legendUpdated');
+        }
+      };
+      $(this.legendChild.el).bind('change', onChangeHandler);
+      $(this.legendChild.el).bind('keyup', onChangeHandler);
+    }
+  }
+
+  // Recurse down the tree to enhance children
+  _.each(this.children, function (child) {
+    child.enhance();
+  });
+};
+
+
+
+/**
+ * Inserts an item in the array at the requested position and renders the item.
+ *
+ * @function
+ * @param {Number} idx Insertion index
+ */
+formNode.prototype.insertArrayItem = function (idx, domElement) {
+  var i = 0;
+
+  // Insert element at the end of the array if index is not given
+  if (idx === undefined) {
+    idx = this.children.length;
+  }
+
+  // Create the additional array item at the end of the list,
+  // using the item template created when tree was initialized
+  // (the call to resetValues ensures that 'arrayPath' is correctly set)
+  var child = this.childTemplate.clone();
+  this.appendChild(child);
+  child.resetValues();
+
+  // To create a blank array item at the requested position,
+  // shift values down starting at the requested position
+  // one to insert (note we start with the end of the array on purpose)
+  for (i = this.children.length-2; i >= idx; i--) {
+    this.children[i].moveValuesTo(this.children[i+1]);
+  }
+
+  // Initialize the blank node we've created with default values
+  this.children[idx].resetValues();
+  this.children[idx].computeInitialValues();
+
+  // Re-render all children that have changed
+  for (i = idx; i < this.children.length; i++) {
+    this.children[i].render(domElement);
+  }
+};
+
+
+/**
+ * Remove an item from an array
+ *
+ * @function
+ * @param {Number} idx The index number of the item to remove
+ */
+formNode.prototype.deleteArrayItem = function (idx) {
+  var i = 0;
+  var child = null;
+
+  // Delete last item if no index is given
+  if (idx === undefined) {
+    idx = this.children.length - 1;
+  }
+
+  // Move values up in the array
+  for (i = idx; i < this.children.length-1; i++) {
+    this.children[i+1].moveValuesTo(this.children[i]);
+    this.children[i].render();
+  }
+
+  // Remove the last array item from the DOM tree and from the form tree
+  this.removeChild();
+};
+
+/**
+ * Returns the minimum/maximum number of items that an array field
+ * is allowed to have according to the schema definition of the fields
+ * it contains.
+ *
+ * The function parses the schema definitions of the array items that
+ * compose the current "array" node and returns the minimum value of
+ * "maxItems" it encounters as the maximum number of items, and the
+ * maximum value of "minItems" as the minimum number of items.
+ *
+ * The function reports a -1 for either of the boundaries if the schema
+ * does not put any constraint on the number of elements the current
+ * array may have of if the current node is not an array.
+ *
+ * Note that array boundaries should be defined in the JSON Schema using
+ * "minItems" and "maxItems". The code also supports "minLength" and
+ * "maxLength" as a fallback, mostly because it used to by mistake (see #22)
+ * and because other people could make the same mistake.
+ *
+ * @function
+ * @return {Object} An object with properties "minItems" and "maxItems"
+ *  that reports the corresponding number of items that the array may
+ *  have (value is -1 when there is no constraint for that boundary)
+ */
+formNode.prototype.getArrayBoundaries = function () {
+  var boundaries = {
+    minItems: -1,
+    maxItems: -1
+  };
+  if (!this.view || !this.view.array) return boundaries;
+
+  var getNodeBoundaries = function (node, initialNode) {
+    var schemaKey = null;
+    var arrayKey = null;
+    var boundaries = {
+      minItems: -1,
+      maxItems: -1
+    };
+    initialNode = initialNode || node;
+
+    if (node.view && node.view.array && (node !== initialNode)) {
+      // New array level not linked to an array in the schema,
+      // so no size constraints
+      return boundaries;
+    }
+
+    if (node.key) {
+      // Note the conversion to target the actual array definition in the
+      // schema where minItems/maxItems may be defined. If we're still looking
+      // at the initial node, the goal is to convert from:
+      //  foo[0].bar[3].baz to foo[].bar[].baz
+      // If we're not looking at the initial node, the goal is to look at the
+      // closest array parent:
+      //  foo[0].bar[3].baz to foo[].bar
+      arrayKey = node.key.replace(/\[[0-9]+\]/g, '[]');
+      if (node !== initialNode) {
+        arrayKey = arrayKey.replace(/\[\][^\[\]]*$/, '');
+      }
+      schemaKey = getSchemaKey(
+        node.ownerTree.formDesc.schema.properties,
+        arrayKey
+      );
+      if (!schemaKey) return boundaries;
+      return {
+        minItems: schemaKey.minItems || schemaKey.minLength || -1,
+        maxItems: schemaKey.maxItems || schemaKey.maxLength || -1
+      };
+    }
+    else {
+      _.each(node.children, function (child) {
+        var subBoundaries = getNodeBoundaries(child, initialNode);
+        if (subBoundaries.minItems !== -1) {
+          if (boundaries.minItems !== -1) {
+            boundaries.minItems = Math.max(
+              boundaries.minItems,
+              subBoundaries.minItems
+            );
+          }
+          else {
+            boundaries.minItems = subBoundaries.minItems;
+          }
+        }
+        if (subBoundaries.maxItems !== -1) {
+          if (boundaries.maxItems !== -1) {
+            boundaries.maxItems = Math.min(
+              boundaries.maxItems,
+              subBoundaries.maxItems
+            );
+          }
+          else {
+            boundaries.maxItems = subBoundaries.maxItems;
+          }
+        }
+      });
+    }
+    return boundaries;
+  };
+  return getNodeBoundaries(this);
+};
+
+
+/**
+ * Form tree class.
+ *
+ * Holds the internal representation of the form.
+ * The tree is always in sync with the rendered form, this allows to parse
+ * it easily.
+ *
+ * @class
+ */
+var formTree = function () {
+  this.eventhandlers = [];
+  this.root = null;
+  this.formDesc = null;
+};
+
+/**
+ * Initializes the form tree structure from the JSONForm object
+ *
+ * This function is the main entry point of the JSONForm library.
+ *
+ * Initialization steps:
+ * 1. the internal tree structure that matches the JSONForm object
+ *  gets created (call to buildTree)
+ * 2. initial values are computed from previously submitted values
+ *  or from the default values defined in the JSON schema.
+ *
+ * When the function returns, the tree is ready to be rendered through
+ * a call to "render".
+ *
+ * @function
+ */
+formTree.prototype.initialize = function (formDesc) {
+  formDesc = formDesc || {};
+
+  // Keep a pointer to the initial JSONForm
+  // (note clone returns a shallow copy, only first-level is cloned)
+  this.formDesc = _.clone(formDesc);
+
+  // Compute form prefix if no prefix is given.
+  this.formDesc.prefix = this.formDesc.prefix ||
+    'jsonform-' + _.uniqueId();
+
+  // JSON schema shorthand
+  if (this.formDesc.schema && !this.formDesc.schema.properties) {
+    this.formDesc.schema = {
+      properties: this.formDesc.schema
+    };
+  }
+
+  // Ensure layout is set
+  this.formDesc.form = this.formDesc.form || [
+    '*',
+    {
+      type: 'actions',
+      items: [
+        {
+          type: 'submit',
+          value: 'Submit'
+        }
+      ]
+    }
+  ];
+  this.formDesc.form = (_.isArray(this.formDesc.form) ?
+    this.formDesc.form :
+    [this.formDesc.form]);
+
+  this.formDesc.params = this.formDesc.params || {};
+
+  // Create the root of the tree
+  this.root = new formNode();
+  this.root.ownerTree = this;
+  this.root.view = jsonform.elementTypes['root'];
+
+  // Generate the tree from the form description
+  this.buildTree();
+
+  // Compute the values associated with each node
+  // (for arrays, the computation actually creates the form nodes)
+  this.computeInitialValues();
+};
+
+
+/**
+ * Constructs the tree from the form description.
+ *
+ * The function must be called once when the tree is first created.
+ *
+ * @function
+ */
+formTree.prototype.buildTree = function () {
+  // Parse and generate the form structure based on the elements encountered:
+  // - '*' means "generate all possible fields using default layout"
+  // - a key reference to target a specific data element
+  // - a more complex object to generate specific form sections
+  _.each(this.formDesc.form, function (formElement) {
+    if (formElement === '*') {
+      _.each(this.formDesc.schema.properties, function (element, key) {
+        this.root.appendChild(this.buildFromLayout({
+          key: key
+        }));
+      }, this);
+    }
+    else {
+      if (_.isString(formElement)) {
+        formElement = {
+          key: formElement
+        };
+      }
+      this.root.appendChild(this.buildFromLayout(formElement));
+    }
+  }, this);
+};
+
+
+/**
+ * Builds the internal form tree representation from the requested layout.
+ *
+ * The function is recursive, generating the node children as necessary.
+ * The function extracts the values from the previously submitted values
+ * (this.formDesc.value) or from default values defined in the schema.
+ *
+ * @function
+ * @param {Object} formElement JSONForm element to render
+ * @param {Object} context The parsing context (the array depth in particular)
+ * @return {Object} The node that matches the element.
+ */
+formTree.prototype.buildFromLayout = function (formElement, context) {
+  var schemaElement = null;
+  var node = new formNode();
+  var view = null;
+  var key = null;
+
+  // The form element parameter directly comes from the initial
+  // JSONForm object. We'll make a shallow copy of it and of its children
+  // not to pollute the original object.
+  // (note JSON.parse(JSON.stringify()) cannot be used since there may be
+  // event handlers in there!)
+  formElement = _.clone(formElement);
+  if (formElement.items) {
+    if (_.isArray(formElement.items)) {
+      formElement.items = _.map(formElement.items, _.clone);
+    }
+    else {
+      formElement.items = [ _.clone(formElement.items) ];
+    }
+  }
+
+  if (formElement.key) {
+    // The form element is directly linked to an element in the JSON
+    // schema. The properties of the form element override those of the
+    // element in the JSON schema. Properties from the JSON schema complete
+    // those of the form element otherwise.
+
+    // Retrieve the element from the JSON schema
+    schemaElement = getSchemaKey(
+      this.formDesc.schema.properties,
+      formElement.key);
+    if (!schemaElement) {
+      // The JSON Form is invalid!
+      throw new Error('The JSONForm object references the schema key "' +
+        formElement.key + '" but that key does not exist in the JSON schema');
+    }
+
+    // Schema element has just been found, let's trigger the
+    // "onElementSchema" event
+    // (tidoust: not sure what the use case for this is, keeping the
+    // code for backward compatibility)
+    if (this.formDesc.onElementSchema) {
+      this.formDesc.onElementSchema(formElement, schemaElement);
+    }
+
+    formElement.name =
+      formElement.name ||
+      formElement.key;
+    formElement.title =
+      formElement.title ||
+      schemaElement.title;
+    formElement.description =
+      formElement.description ||
+      schemaElement.description;
+    formElement.readOnly =
+      formElement.readOnly ||
+      schemaElement.readOnly ||
+      formElement.readonly ||
+      schemaElement.readonly;
+
+    // Compute the ID of the input field
+    if (!formElement.id) {
+      formElement.id = escapeSelector(this.formDesc.prefix) +
+        '-elt-' + slugify(formElement.key);
+    }
+
+    // Should empty strings be included in the final value?
+    // TODO: it's rather unclean to pass it through the schema.
+    if (formElement.allowEmpty) {
+      schemaElement._jsonform_allowEmpty = true;
+    }
+
+    // If the form element does not define its type, use the type of
+    // the schema element.
+    if (!formElement.type) {
+      // If schema type is an array containing only a type and "null",
+      // remove null and make the element non-required
+      if (_.isArray(schemaElement.type)) {
+        if (_.contains(schemaElement.type, "null")) {
+          schemaElement.type = _.without(schemaElement.type, "null");
+          schemaElement.required = false;
+        }
+        if (schemaElement.type.length > 1) {
+          throw new Error("Cannot process schema element with multiple types.");
+        }
+        schemaElement.type = _.first(schemaElement.type);
+      }
+
+      if ((schemaElement.type === 'string') &&
+        (schemaElement.format === 'color')) {
+        formElement.type = 'color';
+      } else if ((schemaElement.type === 'number' ||
+        schemaElement.type === 'integer') &&
+        !schemaElement['enum']) {
+       formElement.type = 'number';
+       if (schemaElement.type === 'number') schemaElement.step = 'any';
+      } else if ((schemaElement.type === 'string' ||
+        schemaElement.type === 'any') &&
+        !schemaElement['enum']) {
+        formElement.type = 'text';
+      } else if (schemaElement.type === 'boolean') {
+        formElement.type = 'checkbox';
+      } else if (schemaElement.type === 'object') {
+        if (schemaElement.properties) {
+          formElement.type = 'fieldset';
+        } else {
+          formElement.type = 'textarea';
+        }
+      } else if (!_.isUndefined(schemaElement['enum'])) {
+        formElement.type = 'select';
+      } else {
+        formElement.type = schemaElement.type;
+      }
+    }
+
+    // Unless overridden in the definition of the form element (or unless
+    // there's a titleMap defined), use the enumeration list defined in
+    // the schema
+    if (!formElement.options && schemaElement['enum']) {
+      if (formElement.titleMap) {
+        formElement.options = _.map(schemaElement['enum'], function (value) {
+          return {
+            value: value,
+            title: hasOwnProperty(formElement.titleMap, value) ? formElement.titleMap[value] : value
+          };
+        });
+      }
+      else {
+        formElement.options = schemaElement['enum'];
+      }
+    }
+
+    // Flag a list of checkboxes with multiple choices
+    if ((formElement.type === 'checkboxes') && schemaElement.items) {
+      var itemsEnum = schemaElement.items['enum'];
+      if (itemsEnum) {
+        schemaElement.items._jsonform_checkboxes_as_array = true;
+      }
+      if (!itemsEnum && schemaElement.items[0]) {
+        itemsEnum = schemaElement.items[0]['enum'];
+        if (itemsEnum) {
+          schemaElement.items[0]._jsonform_checkboxes_as_array = true;
+        }
+      }
+    }
+
+    // If the form element targets an "object" in the JSON schema,
+    // we need to recurse through the list of children to create an
+    // input field per child property of the object in the JSON schema
+    if (schemaElement.type === 'object') {
+      _.each(schemaElement.properties, function (prop, propName) {
+        node.appendChild(this.buildFromLayout({
+          key: formElement.key + '.' + propName
+        }));
+      }, this);
+    }
+  }
+
+  if (!formElement.type) {
+    formElement.type = 'none';
+  }
+  view = jsonform.elementTypes[formElement.type];
+  if (!view) {
+    throw new Error('The JSONForm contains an element whose type is unknown: "' +
+      formElement.type + '"');
+  }
+
+
+  if (schemaElement) {
+    // The form element is linked to an element in the schema.
+    // Let's make sure the types are compatible.
+    // In particular, the element must not be a "container"
+    // (or must be an "object" or "array" container)
+    if (!view.inputfield && !view.array &&
+      (formElement.type !== 'selectfieldset') &&
+      (schemaElement.type !== 'object')) {
+      throw new Error('The JSONForm contains an element that links to an ' +
+        'element in the JSON schema (key: "' + formElement.key + '") ' +
+        'and that should not based on its type ("' + formElement.type + '")');
+    }
+  }
+  else {
+    // The form element is not linked to an element in the schema.
+    // This means the form element must be a "container" element,
+    // and must not define an input field.
+    if (view.inputfield && (formElement.type !== 'selectfieldset')) {
+      throw new Error('The JSONForm defines an element of type ' +
+        '"' + formElement.type + '" ' +
+        'but no "key" property to link the input field to the JSON schema');
+    }
+  }
+
+  // A few characters need to be escaped to use the ID as jQuery selector
+  formElement.iddot = escapeSelector(formElement.id || '');
+
+  // Initialize the form node from the form element and schema element
+  node.formElement = formElement;
+  node.schemaElement = schemaElement;
+  node.view = view;
+  node.ownerTree = this;
+
+  // Set event handlers
+  if (!formElement.handlers) {
+    formElement.handlers = {};
+  }
+
+  // Parse children recursively
+  if (node.view.array) {
+    // The form element is an array. The number of items in an array
+    // is by definition dynamic, up to the form user (through "Add more",
+    // "Delete" commands). The positions of the items in the array may
+    // also change over time (through "Move up", "Move down" commands).
+    //
+    // The form node stores a "template" node that serves as basis for
+    // the creation of an item in the array.
+    //
+    // Array items may be complex forms themselves, allowing for nesting.
+    //
+    // The initial values set the initial number of items in the array.
+    // Note a form element contains at least one item when it is rendered.
+    if (formElement.items) {
+      key = formElement.items[0] || formElement.items;
+    }
+    else {
+      key = formElement.key + '[]';
+    }
+    if (_.isString(key)) {
+      key = { key: key };
+    }
+    node.setChildTemplate(this.buildFromLayout(key));
+  }
+  else if (formElement.items) {
+    // The form element defines children elements
+    _.each(formElement.items, function (item) {
+      if (_.isString(item)) {
+        item = { key: item };
+      }
+      node.appendChild(this.buildFromLayout(item));
+    }, this);
+  }
+
+  return node;
+};
+
+
+/**
+ * Computes the values associated with each input field in the tree based
+ * on previously submitted values or default values in the JSON schema.
+ *
+ * For arrays, the function actually creates and inserts additional
+ * nodes in the tree based on previously submitted values (also ensuring
+ * that the array has at least one item).
+ *
+ * The function sets the array path on all nodes.
+ * It should be called once in the lifetime of a form tree right after
+ * the tree structure has been created.
+ *
+ * @function
+ */
+formTree.prototype.computeInitialValues = function () {
+  this.root.computeInitialValues(this.formDesc.value);
+};
+
+
+/**
+ * Renders the form tree
+ *
+ * @function
+ * @param {Node} domRoot The "form" element in the DOM tree that serves as
+ *  root for the form
+ */
+formTree.prototype.render = function (domRoot) {
+  if (!domRoot) return;
+  this.domRoot = domRoot;
+  this.root.render();
+
+  // If the schema defines required fields, flag the form with the
+  // "jsonform-hasrequired" class for styling purpose
+  // (typically so that users may display a legend)
+  if (this.hasRequiredField()) {
+    $(domRoot).addClass('jsonform-hasrequired');
+  }
+};
+
+/**
+ * Walks down the element tree with a callback
+ *
+ * @function
+ * @param {Function} callback The callback to call on each element
+ */
+formTree.prototype.forEachElement = function (callback) {
+
+  var f = function(root) {
+    for (var i=0;i<root.children.length;i++) {
+      callback(root.children[i]);
+      f(root.children[i]);
+    }
+  };
+  f(this.root);
+
+};
+
+formTree.prototype.validate = function(noErrorDisplay) {
+
+  var values = jsonform.getFormValue(this.domRoot);
+  var errors = false;
+
+  var options = this.formDesc;
+
+  if (options.validate!==false) {
+    var validator = false;
+    if (typeof options.validate!="object") {
+      if (global.JSONFormValidator) {
+        validator = global.JSONFormValidator.createEnvironment("json-schema-draft-03");
+      }
+    } else {
+      validator = options.validate;
+    }
+    if (validator) {
+      var v = validator.validate(values, this.formDesc.schema);
+      $(this.domRoot).jsonFormErrors(false,options);
+      if (v.errors.length) {
+        if (!errors) errors = [];
+        errors = errors.concat(v.errors);
+      }
+    }
+  }
+
+  if (errors && !noErrorDisplay) {
+    if (options.displayErrors) {
+      options.displayErrors(errors,this.domRoot);
+    } else {
+      $(this.domRoot).jsonFormErrors(errors,options);
+    }
+  }
+
+  return {"errors":errors}
+
+}
+
+formTree.prototype.submit = function(evt) {
+
+  var stopEvent = function() {
+    if (evt) {
+      evt.preventDefault();
+      evt.stopPropagation();
+    }
+    return false;
+  };
+  var values = jsonform.getFormValue(this.domRoot);
+  var options = this.formDesc;
+
+  var brk=false;
+  this.forEachElement(function(elt) {
+    if (brk) return;
+    if (elt.view.onSubmit) {
+      brk = !elt.view.onSubmit(evt, elt); //may be called multiple times!!
+    }
+  });
+
+  if (brk) return stopEvent();
+
+  var validated = this.validate();
+
+  if (options.onSubmit && !options.onSubmit(validated.errors,values)) {
+    return stopEvent();
+  }
+
+  if (validated.errors) return stopEvent();
+
+  if (options.onSubmitValid && !options.onSubmitValid(values)) {
+    return stopEvent();
+  }
+
+  return false;
+
+};
+
+
+/**
+ * Returns true if the form displays a "required" field.
+ *
+ * To keep things simple, the function parses the form's schema and returns
+ * true as soon as it finds a "required" flag even though, in theory, that
+ * schema key may not appear in the final form.
+ *
+ * Note that a "required" constraint on a boolean type is always enforced,
+ * the code skips such definitions.
+ *
+ * @function
+ * @return {boolean} True when the form has some required field,
+ *  false otherwise.
+ */
+formTree.prototype.hasRequiredField = function () {
+  var parseElement = function (element) {
+    if (!element) return null;
+    if (element.required && (element.type !== 'boolean')) {
+      return element;
+    }
+
+    var prop = _.find(element.properties, function (property) {
+      return parseElement(property);
+    });
+    if (prop) {
+      return prop;
+    }
+
+    if (element.items) {
+      if (_.isArray(element.items)) {
+        prop = _.find(element.items, function (item) {
+          return parseElement(item);
+        });
+      }
+      else {
+        prop = parseElement(element.items);
+      }
+      if (prop) {
+        return prop;
+      }
+    }
+  };
+
+  return parseElement(this.formDesc.schema);
+};
+
+
+/**
+ * Returns the structured object that corresponds to the form values entered
+ * by the use for the given form.
+ *
+ * The form must have been previously rendered through a call to jsonform.
+ *
+ * @function
+ * @param {Node} The <form> tag in the DOM
+ * @return {Object} The object that follows the data schema and matches the
+ *  values entered by the user.
+ */
+jsonform.getFormValue = function (formelt) {
+  var form = $(formelt).data('jsonform-tree');
+  if (!form) return null;
+  return form.root.getFormValues();
+};
+
+
+/**
+ * Highlights errors reported by the JSON schema validator in the document.
+ *
+ * @function
+ * @param {Object} errors List of errors reported by the JSON schema validator
+ * @param {Object} options The JSON Form object that describes the form
+ *  (unused for the time being, could be useful to store example values or
+ *   specific error messages)
+ */
+$.fn.jsonFormErrors = function(errors, options) {
+  $(".error", this).removeClass("error");
+  $(".warning", this).removeClass("warning");
+
+  $(".jsonform-errortext", this).hide();
+  if (!errors) return;
+
+  var errorSelectors = [];
+  for (var i = 0; i < errors.length; i++) {
+    // Compute the address of the input field in the form from the URI
+    // returned by the JSON schema validator.
+    // These URIs typically look like:
+    //  urn:uuid:cccc265e-ffdd-4e40-8c97-977f7a512853#/pictures/1/thumbnail
+    // What we need from that is the path in the value object:
+    //  pictures[1].thumbnail
+    // ... and the jQuery-friendly class selector of the input field:
+    //  .jsonform-error-pictures\[1\]---thumbnail
+    var key = errors[i].uri
+      .replace(/.*#\//, '')
+      .replace(/\//g, '.')
+      .replace(/\.([0-9]+)(?=\.|$)/g, '[$1]');
+    var errormarkerclass = ".jsonform-error-" +
+      escapeSelector(key.replace(/\./g,"---"));
+    errorSelectors.push(errormarkerclass);
+
+    var errorType = errors[i].type || "error";
+    $(errormarkerclass, this).addClass(errorType);
+    $(errormarkerclass + " .jsonform-errortext", this).html(errors[i].message).show();
+  }
+
+  // Look for the first error in the DOM and ensure the element
+  // is visible so that the user understands that something went wrong
+  errorSelectors = errorSelectors.join(',');
+  var firstError = $(errorSelectors).get(0);
+  if (firstError && firstError.scrollIntoView) {
+    firstError.scrollIntoView(true, {
+      behavior: 'smooth'
+    });
+  }
+};
+
+
+/**
+ * Generates the HTML form from the given JSON Form object and renders the form.
+ *
+ * Main entry point of the library. Defined as a jQuery function that typically
+ * needs to be applied to a <form> element in the document.
+ *
+ * The function handles the following properties for the JSON Form object it
+ * receives as parameter:
+ * - schema (required): The JSON Schema that describes the form to render
+ * - form: The options form layout description, overrides default layout
+ * - prefix: String to use to prefix computed IDs. Default is an empty string.
+ *  Use this option if JSON Form is used multiple times in an application with
+ *  schemas that have overlapping parameter names to avoid running into multiple
+ *  IDs issues. Default value is "jsonform-[counter]".
+ * - transloadit: Transloadit parameters when transloadit is used
+ * - validate: Validates form against schema upon submission. Uses the value
+ * of the "validate" property as validator if it is an object.
+ * - displayErrors: Function to call with errors upon form submission.
+ *  Default is to render the errors next to the input fields.
+ * - submitEvent: Name of the form submission event to bind to.
+ *  Default is "submit". Set this option to false to avoid event binding.
+ * - onSubmit: Callback function to call when form is submitted
+ * - onSubmitValid: Callback function to call when form is submitted without
+ *  errors.
+ *
+ * @function
+ * @param {Object} options The JSON Form object to use as basis for the form
+ */
+$.fn.jsonForm = function(options) {
+  var formElt = this;
+
+  options = _.defaults({}, options, {submitEvent: 'submit'});
+
+  var form = new formTree();
+  form.initialize(options);
+  form.render(formElt.get(0));
+
+  // TODO: move that to formTree.render
+  if (options.transloadit) {
+    formElt.append('<input type="hidden" name="params" value=\'' +
+      escapeHTML(JSON.stringify(options.transloadit.params)) +
+      '\'>');
+  }
+
+  // Keep a direct pointer to the JSON schema for form submission purpose
+  formElt.data("jsonform-tree", form);
+
+  if (options.submitEvent) {
+    formElt.unbind((options.submitEvent)+'.jsonform');
+    formElt.bind((options.submitEvent)+'.jsonform', function(evt) {
+      form.submit(evt);
+    });
+  }
+
+  // Initialize tabs sections, if any
+  initializeTabs(formElt);
+
+  // Initialize expandable sections, if any
+  $('.expandable > div, .expandable > fieldset', formElt).hide();
+  formElt.on('click', '.expandable > legend', function () {
+    var parent = $(this).parent();
+    parent.toggleClass('expanded');
+    $('> div', parent).slideToggle(100);
+  });
+
+  return form;
+};
+
+
+/**
+ * Retrieves the structured values object generated from the values
+ * entered by the user and the data schema that gave birth to the form.
+ *
+ * Defined as a jQuery function that typically needs to be applied to
+ * a <form> element whose content has previously been generated by a
+ * call to "jsonForm".
+ *
+ * Unless explicitly disabled, the values are automatically validated
+ * against the constraints expressed in the schema.
+ *
+ * @function
+ * @return {Object} Structured values object that matches the user inputs
+ *  and the data schema.
+ */
+$.fn.jsonFormValue = function() {
+  return jsonform.getFormValue(this);
+};
+
+// Expose the getFormValue method to the global object
+// (other methods exposed as jQuery functions)
+global.JSONForm = global.JSONForm || {util:{}};
+global.JSONForm.getFormValue = jsonform.getFormValue;
+global.JSONForm.fieldTemplate = jsonform.fieldTemplate;
+global.JSONForm.fieldTypes = jsonform.elementTypes;
+global.JSONForm.getInitialValue = getInitialValue;
+global.JSONForm.util.getObjKey = jsonform.util.getObjKey;
+global.JSONForm.util.setObjKey = jsonform.util.setObjKey;
+
+})((typeof exports !== 'undefined'),
+  ((typeof exports !== 'undefined') ? exports : window),
+  ((typeof jQuery !== 'undefined') ? jQuery : { fn: {} }),
+  ((typeof _ !== 'undefined') ? _ : null),
+  JSON);

+ 68 - 6
src/http/portal.js

@@ -1,12 +1,14 @@
 portal = new function(){
-    this.setWifi = async () => {
+    this.saveSettings = async () => {
         const ssid = getId("input_ssid").value;
         const pw   = getId("input_pw").value;
+        const sshd   = getId("input_sshd").value;
         const resp = await postData("", {
-            "set_wifi":{
+            "save_settings":{
                 "ssid":ssid,
                 "pw":pw,
-            }
+            },
+            "sshd": sshd
         })
         error_box.value = JSON.stringify(resp, null, 2);
     }
@@ -67,19 +69,79 @@ portal = new function(){
             getId("input_ssid").value == "" ||
             getId("input_pw").value == ""
         ){
-            getId("wifi_button").disabled = true;
+            getId("save_button").disabled = true;
         } else {
-            getId("wifi_button").disabled = false;
+            getId("save_button").disabled = false;
         }
     }
     window.addEventListener("load",async ()=>{
+        const resp_form = await postData("", {
+            "get_form":true
+        })
+        const form_div = getId("form_div");
+        while (form_div.childElementCount){
+            form_div.removeChild(form_div.firstChild);
+        };
+        var form = resp_form.js;
+        form.forEach((full_form)=>{
+            if (full_form.ok != true){
+                var err_dom = document.createElement("pre");
+                err_dom.innerText = full_form.err;
+                getId("form_div").appendChild(err_dom);
+                return;
+            } 
+            full_form.form.push({
+                "type": "submit",
+                "title": "submit",
+            });
+            full_form["onSubmit"] = async (errors, values) => {
+                    event.preventDefault();
+                    console.log(values);
+                    if (errors){
+                        console.log(errors);
+                    } else {
+                        var data = {
+                            "submit_form": values,
+                        };
+                        data.submit_form["name"] = full_form.name;
+                        var resp = await postData("", data);
+                        var err_dom = document.createElement("pre");
+                        err_dom.innerText = JSON.stringify(resp);
+                        getId("form_div").appendChild(err_dom);
+                    }
+                }
+            console.log(full_form);
+            var form_dom = document.createElement("form");
+            $(form_dom).jsonForm(full_form);
+            var matches = form_dom.getElementsByClassName("btn-default");
+            for (var i=0; i<matches.length; i++) {
+                matches[i].classList.add("btn-secondary");
+            };
+            form_dom.classList.add("collapse")
+            form_dom.classList.add("card-body")
+            var form_collapse = document.createElement("div");
+            form_collapse.classList.add("card")
+            var form_button = document.createElement("button");
+            form_button.classList.add("btn")
+            form_button.classList.add("btn-primary")
+            form_button.textContent = full_form.name;
+            form_button.onclick = () => {
+                form_dom.classList.toggle("show");
+            };
+            form_collapse.appendChild(form_button);
+            form_collapse.appendChild(form_dom);
+            getId("form_div").appendChild(form_collapse);
+        });
+        /*
         getId("input_ssid").addEventListener("input",validate_input)
         getId("input_pw").addEventListener("input",validate_input);
         validate_input();
         const resp = await postData("", {
-            "index_data":true
+            "get_data":true
         })
+        $('form').jsonForm(resp);
         getId("input_ssid").value = resp["ssid"];
         getId("input_pw").value   = resp["pw"];
+        */
     });
 }();