UNPKG

artoo-js

Version:

The client-side scraping companion.

2,059 lines (1,722 loc) 90.7 kB
;(function(undefined) { 'use strict'; /** * artoo core * =========== * * The main artoo namespace and its vital properties. */ // Checking whether a body exists var body; if ('document' in this) { body = document.getElementsByTagName('body')[0]; if (!body) { body = document.createElement('body'); document.documentElement.appendChild(body); } } // Main object var artoo = { // Standard properties $: {}, jquery: { applyPlugins: function() { artoo.jquery.plugins.map(function(p) { p(artoo.$); }); }, plugins: [] }, mountNode: body, stylesheets: {}, templates: {}, // Emitter shim properties _enabled: true, _children: [], _handlers: {}, _handlersAll: [] }; // Non-writable version Object.defineProperty(artoo, 'version', { value: '0.4.4' }); // Exporting to global scope this.artoo = artoo; }).call(this); (function() { 'use strict'; /** * Here is the list of every allowed parameter when using Emitter#on: * @type {Object} */ var __allowedOptions = { once: 'boolean', scope: 'object' }; /** * The emitter's constructor. It initializes the handlers-per-events store and * the global handlers store. * * Emitters are useful for non-DOM events communication. Read its methods * documentation for more information about how it works. * * @return {Emitter} The fresh new instance. */ var Emitter = function() { this._enabled = true; this._children = []; this._handlers = {}; this._handlersAll = []; }; /** * This method binds one or more functions to the emitter, handled to one or a * suite of events. So, these functions will be executed anytime one related * event is emitted. * * It is also possible to bind a function to any emitted event by not * specifying any event to bind the function to. * * Recognized options: * ******************* * - {?boolean} once If true, the handlers will be unbound after the first * execution. Default value: false. * - {?object} scope If a scope is given, then the listeners will be called * with this scope as "this". * * Variant 1: * ********** * > myEmitter.on('myEvent', function(e) { console.log(e); }); * > // Or: * > myEmitter.on('myEvent', function(e) { console.log(e); }, { once: true }); * * @param {string} event The event to listen to. * @param {function} handler The function to bind. * @param {?object} options Eventually some options. * @return {Emitter} Returns this. * * Variant 2: * ********** * > myEmitter.on( * > ['myEvent1', 'myEvent2'], * > function(e) { console.log(e); } * >); * > // Or: * > myEmitter.on( * > ['myEvent1', 'myEvent2'], * > function(e) { console.log(e); } * > { once: true }} * >); * * @param {array} events The events to listen to. * @param {function} handler The function to bind. * @param {?object} options Eventually some options. * @return {Emitter} Returns this. * * Variant 3: * ********** * > myEmitter.on({ * > myEvent1: function(e) { console.log(e); }, * > myEvent2: function(e) { console.log(e); } * > }); * > // Or: * > myEmitter.on({ * > myEvent1: function(e) { console.log(e); }, * > myEvent2: function(e) { console.log(e); } * > }, { once: true }); * * @param {object} bindings An object containing pairs event / function. * @param {?object} options Eventually some options. * @return {Emitter} Returns this. * * Variant 4: * ********** * > myEmitter.on(function(e) { console.log(e); }); * > // Or: * > myEmitter.on(function(e) { console.log(e); }, { once: true}); * * @param {function} handler The function to bind to every events. * @param {?object} options Eventually some options. * @return {Emitter} Returns this. */ Emitter.prototype.on = function(a, b, c) { var i, l, k, event, eArray, bindingObject; // Variant 1 and 2: if (typeof b === 'function') { eArray = typeof a === 'string' ? [a] : a; for (i = 0, l = eArray.length; i !== l; i += 1) { event = eArray[i]; // Check that event is not '': if (!event) continue; if (!this._handlers[event]) this._handlers[event] = []; bindingObject = { handler: b }; for (k in c || {}) if (__allowedOptions[k]) bindingObject[k] = c[k]; else throw new Error( 'The option "' + k + '" is not recognized by Emmett.' ); this._handlers[event].push(bindingObject); } // Variant 3: } else if (a && typeof a === 'object' && !Array.isArray(a)) for (event in a) Emitter.prototype.on.call(this, event, a[event], b); // Variant 4: else if (typeof a === 'function') { bindingObject = { handler: a }; for (k in c || {}) if (__allowedOptions[k]) bindingObject[k] = c[k]; else throw new Error( 'The option "' + k + '" is not recognized by Emmett.' ); this._handlersAll.push(bindingObject); } // No matching variant: else throw new Error('Wrong arguments.'); return this; }; /** * This method works exactly as the previous #on, but will add an options * object if none is given, and set the option "once" to true. * * The polymorphism works exactly as with the #on method. */ Emitter.prototype.once = function(a, b, c) { // Variant 1 and 2: if (typeof b === 'function') { c = c || {}; c.once = true; this.on(a, b, c); // Variants 3 and 4: } else if ( // Variant 3: (a && typeof a === 'object' && !Array.isArray(a)) || // Variant 4: (typeof a === 'function') ) { b = b || {}; b.once = true; this.on(a, b); // No matching variant: } else throw new Error('Wrong arguments.'); return this; }; /** * This method unbinds one or more functions from events of the emitter. So, * these functions will no more be executed when the related events are * emitted. If the functions were not bound to the events, nothing will * happen, and no error will be thrown. * * Variant 1: * ********** * > myEmitter.off('myEvent', myHandler); * * @param {string} event The event to unbind the handler from. * @param {function} handler The function to unbind. * @return {Emitter} Returns this. * * Variant 2: * ********** * > myEmitter.off(['myEvent1', 'myEvent2'], myHandler); * * @param {array} events The events to unbind the handler from. * @param {function} handler The function to unbind. * @return {Emitter} Returns this. * * Variant 3: * ********** * > myEmitter.off({ * > myEvent1: myHandler1, * > myEvent2: myHandler2 * > }); * * @param {object} bindings An object containing pairs event / function. * @return {Emitter} Returns this. * * Variant 4: * ********** * > myEmitter.off(myHandler); * * @param {function} handler The function to unbind from every events. * @return {Emitter} Returns this. */ Emitter.prototype.off = function(events, handler) { var i, n, j, m, k, a, event, eArray = typeof events === 'string' ? [events] : events; if (arguments.length === 1 && typeof eArray === 'function') { handler = arguments[0]; // Handlers bound to events: for (k in this._handlers) { a = []; for (i = 0, n = this._handlers[k].length; i !== n; i += 1) if (this._handlers[k][i].handler !== handler) a.push(this._handlers[k][i]); this._handlers[k] = a; } a = []; for (i = 0, n = this._handlersAll.length; i !== n; i += 1) if (this._handlersAll[i].handler !== handler) a.push(this._handlersAll[i]); this._handlersAll = a; } else if (arguments.length === 2) { for (i = 0, n = eArray.length; i !== n; i += 1) { event = eArray[i]; if (this._handlers[event]) { a = []; for (j = 0, m = this._handlers[event].length; j !== m; j += 1) if (this._handlers[event][j].handler !== handler) a.push(this._handlers[event][j]); this._handlers[event] = a; } if (this._handlers[event] && this._handlers[event].length === 0) delete this._handlers[event]; } } return this; }; /** * This method unbinds every handlers attached to every or any events. So, * these functions will no more be executed when the related events are * emitted. If the functions were not bound to the events, nothing will * happen, and no error will be thrown. * * Usage: * ****** * > myEmitter.unbindAll(); * * @return {Emitter} Returns this. */ Emitter.prototype.unbindAll = function() { var k; this._handlersAll = []; for (k in this._handlers) delete this._handlers[k]; return this; }; /** * This method emits the specified event(s), and executes every handlers bound * to the event(s). * * Use cases: * ********** * > myEmitter.emit('myEvent'); * > myEmitter.emit('myEvent', myData); * > myEmitter.emit(['myEvent1', 'myEvent2']); * > myEmitter.emit(['myEvent1', 'myEvent2'], myData); * * @param {string|array} events The event(s) to emit. * @param {object?} data The data. * @return {Emitter} Returns this. */ Emitter.prototype.emit = function(events, data) { var i, n, j, m, z, a, event, child, handlers, eventName, self = this, eArray = typeof events === 'string' ? [events] : events; // Check that the emitter is enabled: if (!this._enabled) return this; data = data === undefined ? {} : data; for (i = 0, n = eArray.length; i !== n; i += 1) { eventName = eArray[i]; handlers = (this._handlers[eventName] || []).concat(this._handlersAll); if (handlers.length) { event = { type: eventName, data: data || {}, target: this }; a = []; for (j = 0, m = handlers.length; j !== m; j += 1) { // We have to verify that the handler still exists in the array, // as it might have been mutated already if ( ( this._handlers[eventName] && this._handlers[eventName].indexOf(handlers[j]) >= 0 ) || this._handlersAll.indexOf(handlers[j]) >= 0 ) { handlers[j].handler.call( 'scope' in handlers[j] ? handlers[j].scope : this, event ); // Since the listener callback can mutate the _handlers, // we register the handlers we want to remove, not the ones // we want to keep if (handlers[j].once) a.push(handlers[j]); } } // Go through handlers to remove for (z = 0; z < a.length; z++) { this._handlers[eventName].splice(a.indexOf(a[z]), 1); } } } // Events propagation: for (i = 0, n = this._children.length; i !== n; i += 1) { child = this._children[i]; child.emit.apply(child, arguments); } return this; }; /** * This method creates a new instance of Emitter and binds it as a child. Here * is what children do: * - When the parent emits an event, the children will emit the same later * - When a child is killed, it is automatically unreferenced from the parent * - When the parent is killed, all children will be killed as well * * @return {Emitter} Returns the fresh new child. */ Emitter.prototype.child = function() { var self = this, child = new Emitter(); child.on('emmett:kill', function() { if (self._children) for (var i = 0, l = self._children.length; i < l; i++) if (self._children[i] === child) { self._children.splice(i, 1); break; } }); this._children.push(child); return child; }; /** * This returns an array of handler functions corresponding to the given * event or every handler functions if an event were not to be given. * * @param {?string} event Name of the event. * @return {Emitter} Returns this. */ function mapHandlers(a) { var i, l, h = []; for (i = 0, l = a.length; i < l; i++) h.push(a[i].handler); return h; } Emitter.prototype.listeners = function(event) { var handlers = [], k, i, l; // If no event is passed, we return every handlers if (!event) { handlers = mapHandlers(this._handlersAll); for (k in this._handlers) handlers = handlers.concat(mapHandlers(this._handlers[k])); // Retrieving handlers per children for (i = 0, l = this._children.length; i < l; i++) handlers = handlers.concat(this._children[i].listeners()); } // Else we only retrieve the needed handlers else { handlers = mapHandlers(this._handlers[event]); // Retrieving handlers per children for (i = 0, l = this._children.length; i < l; i++) handlers = handlers.concat(this._children[i].listeners(event)); } return handlers; }; /** * This method will first dispatch a "emmett:kill" event, and then unbinds all * listeners and make it impossible to ever rebind any listener to any event. */ Emitter.prototype.kill = function() { this.emit('emmett:kill'); this.unbindAll(); this._handlers = null; this._handlersAll = null; this._enabled = false; if (this._children) for (var i = 0, l = this._children.length; i < l; i++) this._children[i].kill(); this._children = null; }; /** * This method disabled the emitter, which means its emit method will do * nothing. * * @return {Emitter} Returns this. */ Emitter.prototype.disable = function() { this._enabled = false; return this; }; /** * This method enables the emitter. * * @return {Emitter} Returns this. */ Emitter.prototype.enable = function() { this._enabled = true; return this; }; /** * Version: */ Emitter.version = '2.1.2'; // Export: artoo.emitter = Emitter; }).call(this); /*! * jQuery Simulate v1.0.1 - simulate browser mouse and keyboard events * https://github.com/jquery/jquery-simulate * * Copyright jQuery Foundation and other contributors * Released under the MIT license. * http://jquery.org/license */ ;(function(undefined) { function _simulate($) { var rkeyEvent = /^key/, rmouseEvent = /^(?:mouse|contextmenu)|click/; $.fn.simulate = function( type, options ) { return this.each(function() { new $.simulate( this, type, options ); }); }; $.simulate = function( elem, type, options ) { var method = $.camelCase( "simulate-" + type ); this.target = elem; this.options = options; if ( this[ method ] ) { this[ method ](); } else { this.simulateEvent( elem, type, options ); } }; $.extend( $.simulate, { keyCode: { BACKSPACE: 8, COMMA: 188, DELETE: 46, DOWN: 40, END: 35, ENTER: 13, ESCAPE: 27, HOME: 36, LEFT: 37, NUMPAD_ADD: 107, NUMPAD_DECIMAL: 110, NUMPAD_DIVIDE: 111, NUMPAD_ENTER: 108, NUMPAD_MULTIPLY: 106, NUMPAD_SUBTRACT: 109, PAGE_DOWN: 34, PAGE_UP: 33, PERIOD: 190, RIGHT: 39, SPACE: 32, TAB: 9, UP: 38 }, buttonCode: { LEFT: 0, MIDDLE: 1, RIGHT: 2 } }); $.extend( $.simulate.prototype, { simulateEvent: function( elem, type, options ) { var event = this.createEvent( type, options ); this.dispatchEvent( elem, type, event, options ); }, createEvent: function( type, options ) { if ( rkeyEvent.test( type ) ) { return this.keyEvent( type, options ); } if ( rmouseEvent.test( type ) ) { return this.mouseEvent( type, options ); } }, mouseEvent: function( type, options ) { var event, eventDoc, doc, body; options = $.extend({ bubbles: true, cancelable: (type !== "mousemove"), view: window, detail: 0, screenX: 0, screenY: 0, clientX: 1, clientY: 1, ctrlKey: false, altKey: false, shiftKey: false, metaKey: false, button: 0, relatedTarget: undefined }, options ); if ( document.createEvent ) { event = document.createEvent( "MouseEvents" ); event.initMouseEvent( type, options.bubbles, options.cancelable, options.view, options.detail, options.screenX, options.screenY, options.clientX, options.clientY, options.ctrlKey, options.altKey, options.shiftKey, options.metaKey, options.button, options.relatedTarget || document.body.parentNode ); // IE 9+ creates events with pageX and pageY set to 0. // Trying to modify the properties throws an error, // so we define getters to return the correct values. if ( event.pageX === 0 && event.pageY === 0 && Object.defineProperty ) { eventDoc = event.relatedTarget.ownerDocument || document; doc = eventDoc.documentElement; body = eventDoc.body; Object.defineProperty( event, "pageX", { get: function() { return options.clientX + ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) - ( doc && doc.clientLeft || body && body.clientLeft || 0 ); } }); Object.defineProperty( event, "pageY", { get: function() { return options.clientY + ( doc && doc.scrollTop || body && body.scrollTop || 0 ) - ( doc && doc.clientTop || body && body.clientTop || 0 ); } }); } } else if ( document.createEventObject ) { event = document.createEventObject(); $.extend( event, options ); // standards event.button uses constants defined here: http://msdn.microsoft.com/en-us/library/ie/ff974877(v=vs.85).aspx // old IE event.button uses constants defined here: http://msdn.microsoft.com/en-us/library/ie/ms533544(v=vs.85).aspx // so we actually need to map the standard back to oldIE event.button = { 0: 1, 1: 4, 2: 2 }[ event.button ] || ( event.button === -1 ? 0 : event.button ); } return event; }, keyEvent: function( type, options ) { var event; options = $.extend({ bubbles: true, cancelable: true, view: window, ctrlKey: false, altKey: false, shiftKey: false, metaKey: false, keyCode: 0, charCode: undefined }, options ); if ( document.createEvent ) { try { event = document.createEvent( "KeyEvents" ); event.initKeyEvent( type, options.bubbles, options.cancelable, options.view, options.ctrlKey, options.altKey, options.shiftKey, options.metaKey, options.keyCode, options.charCode ); // initKeyEvent throws an exception in WebKit // see: http://stackoverflow.com/questions/6406784/initkeyevent-keypress-only-works-in-firefox-need-a-cross-browser-solution // and also https://bugs.webkit.org/show_bug.cgi?id=13368 // fall back to a generic event until we decide to implement initKeyboardEvent } catch( err ) { event = document.createEvent( "Events" ); event.initEvent( type, options.bubbles, options.cancelable ); $.extend( event, { view: options.view, ctrlKey: options.ctrlKey, altKey: options.altKey, shiftKey: options.shiftKey, metaKey: options.metaKey, keyCode: options.keyCode, charCode: options.charCode }); } } else if ( document.createEventObject ) { event = document.createEventObject(); $.extend( event, options ); } if ( !!/msie [\w.]+/.exec( navigator.userAgent.toLowerCase() ) || (({}).toString.call( window.opera ) === "[object Opera]") ) { event.keyCode = (options.charCode > 0) ? options.charCode : options.keyCode; event.charCode = undefined; } return event; }, dispatchEvent: function( elem, type, event ) { if ( elem[ type ] ) { elem[ type ](); } else if ( elem.dispatchEvent ) { elem.dispatchEvent( event ); } else if ( elem.fireEvent ) { elem.fireEvent( "on" + type, event ); } }, simulateFocus: function() { var focusinEvent, triggered = false, element = $( this.target ); function trigger() { triggered = true; } element.bind( "focus", trigger ); element[ 0 ].focus(); if ( !triggered ) { focusinEvent = $.Event( "focusin" ); focusinEvent.preventDefault(); element.trigger( focusinEvent ); element.triggerHandler( "focus" ); } element.unbind( "focus", trigger ); }, simulateBlur: function() { var focusoutEvent, triggered = false, element = $( this.target ); function trigger() { triggered = true; } element.bind( "blur", trigger ); element[ 0 ].blur(); // blur events are async in IE setTimeout(function() { // IE won't let the blur occur if the window is inactive if ( element[ 0 ].ownerDocument.activeElement === element[ 0 ] ) { element[ 0 ].ownerDocument.body.focus(); } // Firefox won't trigger events if the window is inactive // IE doesn't trigger events if we had to manually focus the body if ( !triggered ) { focusoutEvent = $.Event( "focusout" ); focusoutEvent.preventDefault(); element.trigger( focusoutEvent ); element.triggerHandler( "blur" ); } element.unbind( "blur", trigger ); }, 1 ); } }); /** complex events **/ function findCenter( elem ) { var offset, document = $( elem.ownerDocument ); elem = $( elem ); offset = elem.offset(); return { x: offset.left + elem.outerWidth() / 2 - document.scrollLeft(), y: offset.top + elem.outerHeight() / 2 - document.scrollTop() }; } function findCorner( elem ) { var offset, document = $( elem.ownerDocument ); elem = $( elem ); offset = elem.offset(); return { x: offset.left - document.scrollLeft(), y: offset.top - document.scrollTop() }; } $.extend( $.simulate.prototype, { simulateDrag: function() { var i = 0, target = this.target, eventDoc = target.ownerDocument, options = this.options, center = options.handle === "corner" ? findCorner( target ) : findCenter( target ), x = Math.floor( center.x ), y = Math.floor( center.y ), coord = { clientX: x, clientY: y }, dx = options.dx || ( options.x !== undefined ? options.x - x : 0 ), dy = options.dy || ( options.y !== undefined ? options.y - y : 0 ), moves = options.moves || 3; this.simulateEvent( target, "mousedown", coord ); for ( ; i < moves ; i++ ) { x += dx / moves; y += dy / moves; coord = { clientX: Math.round( x ), clientY: Math.round( y ) }; this.simulateEvent( eventDoc, "mousemove", coord ); } if ( $.contains( eventDoc, target ) ) { this.simulateEvent( target, "mouseup", coord ); this.simulateEvent( target, "click", coord ); } else { this.simulateEvent( eventDoc, "mouseup", coord ); } } }); } // Exporting artoo.jquery.plugins.push(_simulate); }).call(this); ;(function(undefined) { 'use strict'; /** * artoo beep * =========== * * Experimental feature designed to make artoo beep. */ var collections = { greet: ['announce', 'excited', 'hello', 'music', 'original', 'welcome'], info: ['determined', 'flourish', 'playful', 'sassy', 'talk', 'whistling'], warn: ['assert', 'laugh', 'question', 'quick', 'strange', 'threat'], error: ['sad', 'scream', 'shocked', 'weep'] }; var sounds = collections.greet .concat(collections.info) .concat(collections.warn) .concat(collections.error); // Helpers function randomInArray(a) { return a[Math.floor(Math.random() * a.length)]; } // Playing the base64 sound artoo.beep = function(a1, a2) { var sound, callback, chosenSound; if (typeof a1 === 'function') { callback = a1; } else { sound = a1; if (typeof a2 === 'function') callback = a2; else if (typeof a2 !== 'undefined') throw Error('artoo.beep: second argument has to be a function.'); } if (artoo.helpers.isArray(sound)) chosenSound = randomInArray(sound); else chosenSound = sound || randomInArray(sounds); if (chosenSound in collections) chosenSound = randomInArray(collections[chosenSound]); if (!~sounds.indexOf(chosenSound)) throw Error('artoo.beep: wrong sound specified.'); var player = new Audio(artoo.settings.beep.endpoint + chosenSound + '.ogg'); if(callback) player.addEventListener('ended', function() { callback(); }); player.play(); }; // Exposing available beeps Object.defineProperty(artoo.beep, 'available', { value: sounds }); // Exposing collections Object.defineProperty(artoo.beep, 'collections', { value: collections }); // Creating shortcuts // NOTE: not using bind here to avoid messing with phantomjs sounds.concat(Object.keys(collections)).forEach(function(s) { artoo.beep[s] = function() { artoo.beep(s); }; }); }).call(this); ;(function(undefined) { 'use strict'; /** * artoo settings * =============== * * artoo default settings that user may override. */ // Defaults artoo.settings = { // Root settings autoInit: true, autoExec: true, chromeExtension: false, env: 'dev', eval: null, reExec: true, reload: false, scriptUrl: null, // Methods settings beep: { endpoint: '//medialab.github.io/artoo/sounds/' }, cache: { delimiter: '%' }, dependencies: [], jquery: { version: '2.1.3', force: false }, log: { beeping: false, enabled: true, level: 'verbose', welcome: true }, store: { engine: 'local' } }; // Setting utility artoo.loadSettings = function(ns) { artoo.settings = artoo.helpers.extend(ns, artoo.settings); }; }).call(this); ;(function(undefined) { 'use strict'; /** * artoo helpers * ============== * * Some useful helpers. */ var _root = this; // Extending Emmett Object.setPrototypeOf = Object.setPrototypeOf || function (obj, proto) { obj.__proto__ = proto; return obj; }; var ee = new artoo.emitter(); Object.setPrototypeOf(artoo, Object.getPrototypeOf(ee)); // Legacy support // TODO: drop this asap artoo.hooks = { trigger: function(name) { artoo.emit(name); } }; /** * Generic Helpers * ---------------- * * Some basic helpers from collection handling to type checking. */ // Useless function function noop() {} // Recursively extend objects function extend() { var i, k, res = {}, l = arguments.length; for (i = l - 1; i >= 0; i--) for (k in arguments[i]) if (res[k] && isPlainObject(arguments[i][k])) res[k] = extend(arguments[i][k], res[k]); else res[k] = arguments[i][k]; return res; } // Is the var an array? function isArray(v) { return v instanceof Array; } // Is the var an object? function isObject(v) { return v instanceof Object; } // Is the var a real NaN function isRealNaN(v) { return isNaN(v) && (typeof v === 'number'); } // Is the var a plain object? function isPlainObject(v) { return v instanceof Object && !(v instanceof Array) && !(v instanceof Function); } // Is a var non primitive? function isNonPrimitive(v) { return isPlainObject(v) || isArray(v); } // Is a var primitive? function isPrimitive(v) { return !isNonScalar(v); } // Get first item of array returning true to given function function first(a, fn, scope) { for (var i = 0, l = a.length; i < l; i++) { if (fn.call(scope || null, a[i])) return a[i]; } return; } // Get the index of an element in an array by function function indexOf(a, fn, scope) { for (var i = 0, l = a.length; i < l; i++) { if (fn.call(scope || null, a[i])) return i; } return -1; } // Retrieve a file extenstion from filename or url function getExtension(url) { var a = url.split('.'); if (a.length === 1 || (a[0] === '' && a.length === 2)) return ''; return a.pop(); } /** * Document Helpers * ----------------- * * Functions to deal with DOM selection and the current document. */ // Checking whether a variable is a jQuery selector function isSelector(v) { return (artoo.$ && v instanceof artoo.$) || (jQuery && v instanceof jQuery) || ($ && v instanceof $); } // Checking whether a variable is a DOM document function isDocument(v) { return v instanceof HTMLDocument || v instanceof XMLDocument; } // Get either string or document and return valid jQuery selection function jquerify(v) { var $ = artoo.$; if (isDocument(v)) return $(v); return $('<div />').append(v); } // Creating an HTML or XML document function createDocument(root, namespace) { if (!root) return document.implementation.createHTMLDocument(); else return document.implementation.createDocument( namespace || null, root, null ); } // Loading an external file the same way the browser would load it from page function getScript(url, async, cb) { if (typeof async === 'function') { cb = async; async = false; } var el = document.createElement('script'); // Script attributes el.type = 'text/javascript'; el.src = url; // Should the script be loaded asynchronously? if (async) el.async = true; // Defining callbacks el.onload = el.onreadystatechange = function() { if ((!this.readyState || this.readyState == 'loaded' || this.readyState == 'complete')) { el.onload = el.onreadystatechange = null; // Removing element from head artoo.mountNode.removeChild(el); if (typeof cb === 'function') cb(); } }; // Appending the script to head artoo.mountNode.appendChild(el); } // Loading an external stylesheet function getStylesheet(data, isUrl, cb) { var el = document.createElement(isUrl ? 'link' : 'style'), head = document.getElementsByTagName('head')[0]; el.type = 'text/css'; if (isUrl) { el.href = data; el.rel = 'stylesheet'; // Waiting for script to load el.onload = el.onreadystatechange = function() { if ((!this.readyState || this.readyState == 'loaded' || this.readyState == 'complete')) { el.onload = el.onreadystatechange = null; if (typeof cb === 'function') cb(); } }; } else { el.innerHTML = data; } // Appending the stylesheet to head head.appendChild(el); } var globalsBlackList = [ '__commandLineAPI', 'applicationCache', 'chrome', 'closed', 'console', 'crypto', 'CSS', 'defaultstatus', 'defaultStatus', 'devicePixelRatio', 'document', 'external', 'frameElement', 'history', 'indexedDB', 'innerHeight', 'innerWidth', 'length', 'localStorage', 'location', 'name', 'offscreenBuffering', 'opener', 'outerHeight', 'outerWidth', 'pageXOffset', 'pageYOffset', 'performance', 'screen', 'screenLeft', 'screenTop', 'screenX', 'screenY', 'scrollX', 'scrollY', 'sessionStorage', 'speechSynthesis', 'status', 'styleMedia' ]; function getGlobalVariables() { var p = Object.getPrototypeOf(_root), o = {}, i; for (i in _root) if (!~i.indexOf('webkit') && !(i in p) && _root[i] !== _root && !(_root[i] instanceof BarProp) && !(_root[i] instanceof Navigator) && !~globalsBlackList.indexOf(i)) o[i] = _root[i]; return o; } /** * Async Helpers * -------------- * * Some helpful functions to deal with asynchronous matters. */ // Waiting for something to happen function waitFor(check, cb, params) { params = params || {}; if (typeof cb === 'object') { params = cb; cb = params.done; } var milliseconds = params.interval || 30, j = 0; var i = setInterval(function() { if (check()) { clearInterval(i); cb(null); } if (params.timeout && params.timeout - (j * milliseconds) <= 0) { clearInterval(i); cb(new Error('timeout')); } j++; }, milliseconds); } // Dispatch asynchronous function function async() { var args = Array.prototype.slice.call(arguments); return setTimeout.apply(null, [args[0], 0].concat(args.slice(1))); } // Launching tasks in parallel with an optional limit function parallel(tasks, params, last) { var onEnd = (typeof params === 'function') ? params : params.done || last, running = [], results = [], d = 0, t, l, i; if (typeof onEnd !== 'function') onEnd = noop; function cleanup() { running.forEach(function(r) { clearTimeout(r); }); } function onTaskEnd(err, result) { // Adding results to accumulator results.push(result); if (err) { cleanup(); return onEnd(err, results); } if (++d >= tasks.length) { // Parallel action is finished, returning return onEnd(null, results); } // Adding on stack t = tasks[i++]; running.push(async(t, onTaskEnd)); } for (i = 0, l = params.limit || tasks.length; i < l; i++) { t = tasks[i]; // Dispatching the function asynchronously running.push(async(t, onTaskEnd)); } } /** * Monkey Patching * ---------------- * * Some monkey patching shortcuts. Useful for sniffers and overriding * native functions. */ function before(targetFunction, beforeFunction) { // Replacing the target function return function() { // Applying our function beforeFunction.apply(this, Array.prototype.slice.call(arguments)); // Applying the original function return targetFunction.apply(this, Array.prototype.slice.call(arguments)); }; } /** * Exportation * ------------ */ // Exporting to artoo root artoo.injectScript = function(url, cb) { getScript(url, cb); }; artoo.injectStyle = function(url, cb) { getStylesheet(url, true, cb); }; artoo.injectInlineStyle = function(text) { getStylesheet(text, false); }; artoo.waitFor = waitFor; artoo.getGlobalVariables = getGlobalVariables; // Exporting to artoo helpers artoo.helpers = { before: before, createDocument: createDocument, extend: extend, first: first, getExtension: getExtension, indexOf: indexOf, isArray: isArray, isDocument: isDocument, isObject: isObject, isPlainObject: isPlainObject, isRealNaN: isRealNaN, isSelector: isSelector, isNonPrimitive: isNonPrimitive, isPrimitive: isPrimitive, jquerify: jquerify, noop: noop, parallel: parallel }; }).call(this); ;(function(undefined) { 'use strict'; /** * artoo parsers * ============== * * Compilation of small parsers aim at understanding some popular web * string formats such as querystrings, headers etc. */ function parseQueryString(s) { var data = {}; s.split('&').forEach(function(item) { var pair = item.split('='); data[decodeURIComponent(pair[0])] = pair[1] ? decodeURIComponent(pair[1]) : true; }); return data; } function parseUrl(url) { var data = {href: url}; // Searching for a protocol var ps = url.split('://'); if (ps.length > 1) data.protocol = ps[0]; url = ps[ps.length > 1 ? 1 : 0]; // Searching for an authentification var a = url.split('@'); if (a.length > 1) { var as = a[0].split(':'); if (as.length > 1) { data.auth = { user: as[0], password: as[1] }; } else { data.auth = { user: as[0] }; } url = a[1]; } // Searching for origin var m = url.match(/([^\/:]+)(.*)/); data.host = m[1]; data.hostname = m[1]; if (m[2]) { var f = m[2].trim(); // Port if (f.charAt(0) === ':') { data.port = +f.match(/\d+/)[0]; data.host += ':' + data.port; } // Path data.path = '/' + f.split('/').slice(1).join('/'); data.pathname = data.path.split('?')[0].split('#')[0]; } // Tld if (~data.hostname.search('.')) { var ds = data.hostname.split('.'); // Check for IP if (!(ds.length === 4 && ds.every(function(i) { return !isNaN(+i); }))) { // Checking TLD-less urls if (ds.length > 1) { // TLD data.tld = ds[ds.length - 1]; // Domain data.domain = ds[ds.length - 2]; // Subdomains if (ds.length > 2) { data.subdomains = []; for (var i = 0, l = ds.length - 2; i < l; i++) data.subdomains.unshift(ds[i]); } } else { // TLD-less url data.domain = ds[0]; } } else { // This is an IP data.domain = data.hostname; } } // Hash var hs = url.split('#'); if (hs.length > 1) { data.hash = '#' + hs[1]; } // Querystring var qs = url.split('?'); if (qs.length > 1) { data.search = '?' + qs[1]; data.query = parseQueryString(qs[1]); } // Extension var ss = data.pathname.split('/'), es = ss[ss.length - 1].split('.'); if (es.length > 1) data.extension = es[es.length - 1]; return data; } function parseHeaders(headers) { var data = {}; headers.split('\n').filter(function(item) { return item.trim(); }).forEach(function(item) { if (item) { var pair = item.split(': '); data[pair[0]] = pair[1]; } }); return data; } function parseCookie(s) { var cookie = { httpOnly: false, secure: false }; if (!s.trim()) return; s.split('; ').forEach(function(item) { // Path if (~item.search(/path=/i)) { cookie.path = item.split('=')[1]; } else if (~item.search(/expires=/i)) { cookie.expires = item.split('=')[1]; } else if (~item.search(/httponly/i) && !~item.search('=')) { cookie.httpOnly = true; } else if (~item.search(/secure/i) && !~item.search('=')) { cookie.secure = true; } else { var is = item.split('='); cookie.key = is[0]; cookie.value = decodeURIComponent(is[1]); } }); return cookie; } function parseCookies(s) { var cookies = {}; if (!s.trim()) return cookies; s.split('; ').forEach(function(item) { var pair = item.split('='); cookies[pair[0]] = decodeURIComponent(pair[1]); }); return cookies; } /** * Exporting */ artoo.parsers = { cookie: parseCookie, cookies: parseCookies, headers: parseHeaders, queryString: parseQueryString, url: parseUrl }; }).call(this); ;(function(undefined) { 'use strict'; /** * artoo writers * ============== * * Compilation of writers for popular formats such as CSV or YAML. */ // Dependencies var isPlainObject = artoo.helpers.isPlainObject, isArray = artoo.helpers.isArray, isPrimitive = artoo.helpers.isPrimitive, isNonPrimitive = artoo.helpers.isNonPrimitive, isRealNaN = artoo.helpers.isRealNaN; /** * CSV * --- * * Converts an array of array or array of objects into a correct * CSV string for exports purposes. * * Exposes some handful options such as choice of delimiters or order * of keys to handle. */ // Convert an object into an array of its properties function objectToArray(o, order) { order = order || Object.keys(o); return order.map(function(k) { return o[k]; }); } // Retrieve an index of keys present in an array of objects function keysIndex(a) { var keys = [], l, k, i; for (i = 0, l = a.length; i < l; i++) for (k in a[i]) if (!~keys.indexOf(k)) keys.push(k); return keys; } // Escape a string for a RegEx function rescape(s) { return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); } // Converting an array of arrays into a CSV string function toCSVString(data, params) { if (data.length === 0) { return ''; } params = params || {}; var header = params.headers || [], plainObject = isPlainObject(data[0]), keys = plainObject && (params.order || keysIndex(data)), oData, i; // Defaults var escape = params.escape || '"', delimiter = params.delimiter || ','; // Dealing with headers polymorphism if (!header.length) if (plainObject && params.headers !== false) header = keys; // Should we append headers oData = (header.length ? [header] : []).concat( plainObject ? data.map(function(e) { return objectToArray(e, keys); }) : data ); // Converting to string return oData.map(function(row) { return row.map(function(item) { // Wrapping escaping characters var i = ('' + (typeof item === 'undefined' ? '' : item)).replace( new RegExp(rescape(escape), 'g'), escape + escape ); // Escaping if needed return ~i.indexOf(delimiter) || ~i.indexOf(escape) || ~i.indexOf('\n') ? escape + i + escape : i; }).join(delimiter); }).join('\n'); } /** * YAML * ---- * * Converts JavaScript data into a YAML string for export purposes. */ // Characters to escape in YAML var ymlEscape = /[:#,\-\[\]\{\}&%]|!{1,2}/; // Creating repeating sequences function repeatString(string, nb) { var s = string, l, i; if (nb <= 0) return ''; for (i = 1, l = nb | 0; i < l; i++) s += string; return s; } // YAML conversion var yml = { string: function(string) { return (~string.search(ymlEscape)) ? '\'' + string.replace(/'/g, '\'\'') + '\'' : string; }, number: function(nb) { return '' + nb; }, array: function(a, lvl) { lvl = lvl || 0; if (!a.length) return '[]'; var string = '', l, i; for (i = 0, l = a.length; i < l; i++) { string += repeatString(' ', lvl); if (isPrimitive(a[i])) { string += '- ' + processYAMLVariable(a[i]) + '\n'; } else { if (isPlainObject(a[i])) string += '-' + processYAMLVariable(a[i], lvl + 1, true); else string += processYAMLVariable(a[i], lvl + 1); } } return string; }, object: function(o, lvl, indent) { lvl = lvl || 0; if (!Object.keys(o).length) return (lvl ? '- ' : '') + '{}'; var string = '', key, c = 0, i; for (i in o) { key = yml.string(i); string += repeatString(' ', lvl); if (indent && !c) string = string.slice(0, -1); string += key + ': ' + (isNonPrimitive(o[i]) ? '\n' : '') + processYAMLVariable(o[i], lvl + 1) + '\n'; c++; } return string; }, fn: function(fn) { return yml.string(fn.toString()); }, boolean: function(v) { return '' + v; }, nullValue: function(v) { return '~'; } }; // Get the correct handler corresponding to variable type function processYAMLVariable(v, lvl, indent) { // Scalars if (typeof v === 'string') return yml.string(v); else if (typeof v === 'number') return yml.number(v); else if (typeof v === 'boolean') return yml.boolean(v); else if (typeof v === 'undefined' || v === null || isRealNaN(v)) return yml.nullValue(v); // Nonscalars else if (isPlainObject(v)) return yml.object(v, lvl, indent); else if (isArray(v)) return yml.array(v, lvl); else if (typeof v === 'function') return yml.fn(v); // Error else throw TypeError('artoo.writers.processYAMLVariable: wrong type.'); } // Converting JavaScript variables to a YAML string function toYAMLString(data) { return '---\n' + processYAMLVariable(data); } /** * Web Formats * ------------ * * Converts JavaScript data into standard web formats such as querystrings. */ function toQueryString(o, fn) { if (!isPlainObject(o)) throw Error('artoo.writers.queryString: wrong arguments.'); var s = '', k; for (k in o) { s += (s ? '&' : '') + k + '=' + encodeURIComponent(typeof fn === 'function' ? fn(o[k]) : o[k]); } return s; } function toCookie(key, value, params) { params = params || {}; var cookie = key + '=' + encodeURIComponent(value); if (params.days) { var date = new Date(); date.setTime(date.getTime() + (params.days * 24 * 60 * 60 * 1000)); cookie += '; expires=' + date.toGMTString(); } if (params.path) cookie += '; path=' + params.path; if (params.domain) cookie += '; domain=' + params.domain; if (params.httpOnly) cookie += '; HttpOnly'; if (params.secure) cookie += '; Secure'; return cookie; } /** * Exporting */ artoo.writers = { cookie: toCookie, csv: toCSVString, queryString: toQueryString, yaml: toYAMLString }; }).call(this); ;(function(undefined) { 'use strict'; /** * artoo browser module * ===================== * * Detects in which browser artoo is loaded and what are its capabilities * so he can adapt gracefully. */ var _root = this, inBrowser = 'navigator' in _root; // Helpers function checkFirebug() { var firebug = true; for (var i in _root.console.__proto__) { firebug = false; break; } return firebug; } function checkNode() { return typeof window === 'undefined' && typeof global !== 'undefined' && typeof module !== 'undefined' && module.exports; } // Browsers artoo.browser = { chrome: 'chrome' in _root, firefox: inBrowser && !!~navigator.userAgent.search(/firefox/i), phantomjs: 'callPhantom' in _root, nodejs: checkNode() }; // Which browser? artoo.browser.which = artoo.helpers.first(Object.keys(artoo.browser), function(b) { return artoo.browser[b]; }) || null; // Debuggers artoo.browser.firebug = checkFirebug(); }).call(this); ;(function(undefined) { 'use strict'; /** * artoo console abstraction * ========================== * * Console abstraction enabling artoo to perform a finer logging job than * standard one. */ var _root = this, enhanced = artoo.browser.chrome || artoo.browser.firebug; // Log levels var levels = { verbose: '#33CCFF', // Cyan debug: '#000099', // Blue info: '#009900', // Green warning: 'orange',