├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE.txt ├── README.md ├── chrome ├── background.js ├── content.js └── icons │ ├── icon128.png │ ├── icon16.png │ ├── icon19-disabled.png │ ├── icon19.png │ ├── icon38-disabled.png │ ├── icon38.png │ └── icon48.png ├── gulpfile.js ├── manifest.json ├── meta.json ├── package-lock.json ├── package.json ├── src ├── artoo.beep.js ├── artoo.browser.js ├── artoo.countermeasures.js ├── artoo.dependencies.js ├── artoo.helpers.js ├── artoo.init.js ├── artoo.js ├── artoo.log.js ├── artoo.parsers.js ├── artoo.settings.js ├── artoo.writers.js ├── chrome │ └── artoo.chrome.js ├── common │ └── artoo.methods.asyncStore.js ├── methods │ ├── artoo.methods.ajaxSniffer.js │ ├── artoo.methods.ajaxSpider.js │ ├── artoo.methods.autoExpand.js │ ├── artoo.methods.autoScroll.js │ ├── artoo.methods.cookies.js │ ├── artoo.methods.save.js │ ├── artoo.methods.scrape.js │ ├── artoo.methods.store.js │ └── artoo.methods.ui.js ├── node │ ├── artoo.node.helpers.js │ ├── artoo.node.js │ ├── artoo.node.require.js │ └── artoo.node.shim.js ├── phantom │ └── artoo.phantom.js └── third_party │ ├── emmett.js │ └── jquery.simulate.js └── test ├── endpoint.js ├── helpers.js ├── lib ├── async.js ├── chai.js ├── jquery-2.1.3.min.js ├── jquery.mockjax.js ├── jquery.xmldom.js ├── mocha.css └── mocha.js ├── resources ├── basic_list.html ├── recursive_issue.html ├── recursive_list.html ├── seachange.html └── table.html ├── suites ├── ajaxSpiders.test.js ├── helpers.test.js ├── node.scrape.test.js ├── node.test.js ├── parsers.test.js ├── scrape.test.js ├── store.test.js └── writers.test.js └── unit.html /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .gitignore 3 | .DS_Store 4 | TODO.md 5 | *.pem 6 | *.log 7 | build/ 8 | meta.json 9 | manifest.json 10 | .travis.yml 11 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | TODO.md 4 | *.pem 5 | *.log 6 | build/bookmarklet/ 7 | build/artoo.min.js 8 | test/ 9 | chrome/ 10 | manifest.json 11 | meta.json 12 | gulpfile.js 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "7" -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014-2015 Guillaume Plique, Sciences-po médialab 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | artoo 4 | 5 |

6 | 7 | **artoo.js** is a piece of JavaScript code meant to be run in your browser's console to provide you with some scraping utilities. 8 | 9 | The library's full documentation is available on [github pages](https://medialab.github.io/artoo). 10 | 11 | ## Contribution 12 | [![Build Status](https://travis-ci.org/medialab/artoo.svg)](https://travis-ci.org/medialab/artoo) 13 | 14 | Contributions are more than welcome. Feel free to submit any pull request as long as you added unit tests if relevant and passed them all. 15 | 16 | To install the development environment, clone your fork and use the following commands: 17 | 18 | # Install dependencies 19 | npm install 20 | 21 | # Testing 22 | npm test 23 | 24 | # Compiling dev & prod bookmarklets 25 | gulp bookmarklets 26 | 27 | # Running a test server hosting the concatenated file 28 | npm start 29 | 30 | # Running a https server hosting the concatenated file 31 | # Note that you'll need some ssl keys (instructions to come...) 32 | npm run https 33 | 34 | ## Authors 35 | **artoo.js** is being developed by [Guillaume Plique](https://github.com/Yomguithereal) @ SciencesPo - [médialab](http://www.medialab.sciences-po.fr/fr/). 36 | 37 | Logo by [Daniele Guido](https://github.com/danieleguido). 38 | 39 | R2D2 ascii logo by [Joan Stark](http://www.geocities.com/spunk1111/) aka `jgs`. 40 | -------------------------------------------------------------------------------- /chrome/background.js: -------------------------------------------------------------------------------- 1 | ;(function(undefined) { 2 | 3 | /** 4 | * artoo background script 5 | * ======================== 6 | * 7 | * This chrome background script check for CSP protection in response 8 | * headers and circumvent them. 9 | */ 10 | 11 | var _globals = { 12 | enabled: true 13 | }; 14 | 15 | var possibleHeaders = [ 16 | 'x-webkit-csp', 17 | 'content-security-policy' 18 | ]; 19 | 20 | chrome.webRequest.onHeadersReceived.addListener( 21 | function(details) { 22 | 23 | // Not overriding when artoo is disabled 24 | if (!_globals.enabled) 25 | return; 26 | 27 | var i, l, o; 28 | 29 | for (i = 0, l = details.responseHeaders.length; i < l; i++) { 30 | o = details.responseHeaders[i]; 31 | 32 | if (~possibleHeaders.indexOf(o.name.toLowerCase())) 33 | o.value = 34 | "default-src *;" + 35 | "script-src * 'unsafe-inline' 'unsafe-eval';" + 36 | "connect-src * 'unsafe-inline' 'unsafe-eval';" + 37 | "style-src * 'unsafe-inline';"; 38 | } 39 | 40 | return { 41 | responseHeaders: details.responseHeaders 42 | }; 43 | }, 44 | { 45 | urls: ['http://*/*', 'https://*/*'], 46 | types: [ 47 | 'main_frame', 48 | 'sub_frame', 49 | 'stylesheet', 50 | 'script', 51 | 'image', 52 | 'object', 53 | 'xmlhttprequest', 54 | 'other' 55 | ] 56 | }, 57 | ['blocking', 'responseHeaders'] 58 | ); 59 | 60 | // Browser action 61 | chrome.browserAction.onClicked.addListener(function() { 62 | 63 | // Changing icon and disabling 64 | if (_globals.enabled) 65 | chrome.browserAction.setIcon( 66 | {path: {'19': 'chrome/icons/icon19-disabled.png', 67 | '38': 'chrome/icons/icon38-disabled.png'}}); 68 | else 69 | chrome.browserAction.setIcon( 70 | {path: {'19': 'chrome/icons/icon19.png', 71 | '38': 'chrome/icons/icon38.png'}}); 72 | 73 | _globals.enabled = !_globals.enabled; 74 | }); 75 | 76 | // Receiving variable requests 77 | chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) { 78 | if (request.variable === 'enabled') 79 | sendResponse({enabled: _globals.enabled}); 80 | }); 81 | 82 | // Exporting to window 83 | this.globals = _globals; 84 | }).call(this); 85 | -------------------------------------------------------------------------------- /chrome/content.js: -------------------------------------------------------------------------------- 1 | ;(function(undefined) { 2 | 3 | /** 4 | * artoo chrome injection 5 | * ======================= 6 | * 7 | * This chrome content script injects artoo in every relevant page when the 8 | * artoo's extension is activated. 9 | */ 10 | 11 | function injectScript() { 12 | 13 | // Creating script element 14 | var script = document.createElement('script'), 15 | body = document.getElementsByTagName('body')[0]; 16 | 17 | script.src = chrome.extension.getURL('build/artoo.chrome.js'); 18 | script.type = 'text/javascript'; 19 | script.id = 'artoo_injected_script'; 20 | script.setAttribute('chrome', 'true'); 21 | 22 | // Appending to body 23 | body.appendChild(script); 24 | } 25 | 26 | // Requesting variables from background page 27 | chrome.runtime.sendMessage({variable: 'enabled'}, function(response) { 28 | 29 | // If artoo is enabled, we inject the script 30 | if (response.enabled) 31 | injectScript(); 32 | }); 33 | 34 | // Listening to page's messages 35 | window.addEventListener('message', function(e) { 36 | // console.log('received', e); 37 | }, false); 38 | }).call(this); 39 | -------------------------------------------------------------------------------- /chrome/icons/icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/medialab/artoo/d6f23080ec8453baaf48006b6705841147669558/chrome/icons/icon128.png -------------------------------------------------------------------------------- /chrome/icons/icon16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/medialab/artoo/d6f23080ec8453baaf48006b6705841147669558/chrome/icons/icon16.png -------------------------------------------------------------------------------- /chrome/icons/icon19-disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/medialab/artoo/d6f23080ec8453baaf48006b6705841147669558/chrome/icons/icon19-disabled.png -------------------------------------------------------------------------------- /chrome/icons/icon19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/medialab/artoo/d6f23080ec8453baaf48006b6705841147669558/chrome/icons/icon19.png -------------------------------------------------------------------------------- /chrome/icons/icon38-disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/medialab/artoo/d6f23080ec8453baaf48006b6705841147669558/chrome/icons/icon38-disabled.png -------------------------------------------------------------------------------- /chrome/icons/icon38.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/medialab/artoo/d6f23080ec8453baaf48006b6705841147669558/chrome/icons/icon38.png -------------------------------------------------------------------------------- /chrome/icons/icon48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/medialab/artoo/d6f23080ec8453baaf48006b6705841147669558/chrome/icons/icon48.png -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'), 2 | artoo = require('gulp-artoo'), 3 | concat = require('gulp-concat'), 4 | uglify = require('gulp-uglify'), 5 | jshint = require('gulp-jshint'), 6 | mocha = require('gulp-mocha'), 7 | phantomMocha = require('gulp-mocha-phantomjs'), 8 | replace = require('gulp-replace'), 9 | rename = require('gulp-rename'), 10 | header = require('gulp-header'), 11 | webserver = require('gulp-webserver'), 12 | pkg = require('./package.json'); 13 | 14 | // Utilities 15 | var jsFiles = [ 16 | 'src/artoo.js', 17 | 'src/third_party/emmett.js', 18 | 'src/third_party/jquery.simulate.js', 19 | 'src/artoo.beep.js', 20 | 'src/artoo.settings.js', 21 | 'src/artoo.helpers.js', 22 | 'src/artoo.parsers.js', 23 | 'src/artoo.writers.js', 24 | 'src/artoo.browser.js', 25 | 'src/artoo.log.js', 26 | 'src/artoo.dependencies.js', 27 | 'src/artoo.countermeasures.js', 28 | 'src/methods/artoo.methods.ajaxSniffer.js', 29 | 'src/methods/artoo.methods.ajaxSpider.js', 30 | 'src/methods/artoo.methods.autoExpand.js', 31 | 'src/methods/artoo.methods.autoScroll.js', 32 | 'src/methods/artoo.methods.cookies.js', 33 | 'src/methods/artoo.methods.save.js', 34 | 'src/methods/artoo.methods.scrape.js', 35 | 'src/methods/artoo.methods.store.js', 36 | 'src/methods/artoo.methods.ui.js', 37 | 'src/artoo.init.js' 38 | ]; 39 | 40 | var nodeFiles = [ 41 | 'src/artoo.js', 42 | 'src/node/artoo.node.shim.js', 43 | 'src/third_party/emmett.js', 44 | 'src/node/artoo.node.js', 45 | 'src/artoo.helpers.js', 46 | 'src/artoo.parsers.js', 47 | 'src/artoo.writers.js', 48 | 'src/node/artoo.node.helpers.js', 49 | 'src/methods/artoo.methods.scrape.js', 50 | 'src/node/artoo.node.require.js' 51 | ]; 52 | 53 | var chromeFiles = [ 54 | 'src/chrome/artoo.chrome.js' 55 | ]; 56 | 57 | var phantomFiles = [ 58 | 'src/phantom/artoo.phantom.js' 59 | ]; 60 | 61 | function lintFilter(i) { 62 | return !~i.indexOf('third_party'); 63 | } 64 | 65 | // Testing 66 | gulp.task('browser-test', function() { 67 | return gulp.src('./test/unit.html') 68 | .pipe(replace( 69 | /[\s\S]*/g, 70 | [''].concat( 71 | jsFiles.slice(0, -1).map(function(path) { 72 | return ' '; 73 | }) 74 | ).concat(' ').join('\n') 75 | )) 76 | .pipe(gulp.dest('./test')) 77 | .pipe(phantomMocha({reporter: 'spec'})); 78 | }); 79 | 80 | // Linting 81 | gulp.task('lint', function() { 82 | var avoidFlags = { 83 | '-W055': true, 84 | '-W040': true, 85 | '-W064': true, 86 | '-W061': true, 87 | '-W103': true, 88 | '-W002': true 89 | }; 90 | 91 | return gulp.src(jsFiles.filter(lintFilter)) 92 | .pipe(jshint(avoidFlags)) 93 | .pipe(jshint.reporter('default')); 94 | }); 95 | 96 | // Building 97 | function build_one(name, files) { 98 | return gulp.src(jsFiles.concat(name !== 'concat' ? files : [])) 99 | .pipe(concat('artoo.' + name + '.js')) 100 | .pipe(gulp.dest('./build')); 101 | } 102 | 103 | function browser() { 104 | return build_one('concat') 105 | .pipe(uglify()) 106 | .pipe(header('/* artoo.js - <%= description %> - Version: <%= version %> - Author: <%= author.name %> - medialab SciencesPo */\n', pkg)) 107 | .pipe(rename('artoo.min.js')) 108 | .pipe(gulp.dest('./build')); 109 | }; 110 | 111 | function chrome() { 112 | return build_one('chrome', chromeFiles); 113 | }; 114 | 115 | function phantom() { 116 | return build_one('phantom', phantomFiles); 117 | }; 118 | 119 | function nodefiles() { 120 | return gulp.src(nodeFiles) 121 | .pipe(concat('artoo.node.js')) 122 | .pipe(gulp.dest('./build')); 123 | }; 124 | 125 | var build = gulp.series(browser, chrome, phantom, nodefiles); 126 | 127 | // Bookmarklets 128 | gulp.task('bookmarklet.dev', function() { 129 | var opts = { 130 | random: true, 131 | loadingText: null, 132 | url: '//localhost:8000/build/artoo.concat.js', 133 | settings: { 134 | env: 'dev', 135 | reload: true 136 | } 137 | }; 138 | 139 | return artoo.blank('bookmarklet.dev.min.js') 140 | .pipe(artoo(opts)) 141 | .pipe(gulp.dest('./build/bookmarklets')); 142 | }); 143 | 144 | gulp.task('bookmarklet.prod', function() { 145 | return artoo.blank('bookmarklet.prod.min.js') 146 | .pipe(artoo()) 147 | .pipe(gulp.dest('./build/bookmarklets')); 148 | }); 149 | 150 | gulp.task('bookmarklet.edge', function() { 151 | return artoo.blank('bookmarklet.edge.min.js') 152 | .pipe(artoo({ 153 | version: 'edge', 154 | loadingText: 'artoo.js edge version is loading...' 155 | })) 156 | .pipe(gulp.dest('./build/bookmarklets')); 157 | }); 158 | 159 | // Watching 160 | gulp.task('watch', gulp.series(build, function() { 161 | gulp.watch(jsFiles, ['build']); 162 | })); 163 | 164 | // Serving 165 | gulp.task('serve', function() { 166 | return gulp.src('./') 167 | .pipe(webserver({ 168 | directoryListing: true 169 | })); 170 | }); 171 | 172 | gulp.task('serve.https', function() { 173 | return gulp.src('./') 174 | .pipe(webserver({ 175 | directoryListing: true, 176 | https: true 177 | })); 178 | }); 179 | 180 | gulp.task('node-test', gulp.series(build, function() { 181 | return gulp.src('./test/endpoint.js') 182 | .pipe(mocha({reporter: 'spec'})); 183 | })); 184 | 185 | exports.build = build 186 | 187 | // Macro-tasks 188 | exports.bookmarklets = gulp.series('bookmarklet.dev', 'bookmarklet.prod', 'bookmarklet.edge'); 189 | var test = gulp.series('browser-test', 'node-test'); 190 | exports.test = test 191 | exports.work = gulp.series('watch', 'serve'); 192 | exports.https = gulp.series('watch', 'serve.https'); 193 | exports.default = gulp.series('lint', test, build); 194 | exports.chrome = chrome 195 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "name": "artoo", 4 | "description": "The client-side scraping companion.", 5 | "version": "0.3.4", 6 | "permissions": [ 7 | "background", 8 | "webRequest", 9 | "webRequestBlocking", 10 | "tabs", 11 | "http://*/*", 12 | "https://*/*" 13 | ], 14 | "icons": { 15 | "16": "chrome/icons/icon16.png", 16 | "48": "chrome/icons/icon48.png", 17 | "128": "chrome/icons/icon128.png" 18 | }, 19 | "browser_action": { 20 | "default_title": "artoo", 21 | "default_icon": { 22 | "19": "chrome/icons/icon19.png", 23 | "38": "chrome/icons/icon38.png" 24 | } 25 | }, 26 | "content_scripts": [ 27 | { 28 | "matches": ["http://*/*", "https://*/*"], 29 | "js": ["chrome/content.js"], 30 | "run_at": "document_end" 31 | } 32 | ], 33 | "background": { 34 | "scripts": ["chrome/background.js"], 35 | "persistent": true 36 | }, 37 | "web_accessible_resources": [ 38 | "build/artoo.chrome.js", 39 | "chrome/background.js" 40 | ], 41 | "content_security_policy": "default-src 'self';", 42 | "minimum_chrome_version": "33" 43 | } 44 | -------------------------------------------------------------------------------- /meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "artoo.js", 3 | "authors": ["Guillaume Plique"], 4 | "url": "https://medialab.github.io/artoo/", 5 | "source": "https://github.com/medialab/artoo", 6 | "licence": "MIT", 7 | "visual": "", 8 | "description": "artoo.js - the client-side scraping companion.", 9 | "doc": "" 10 | } 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "artoo-js", 3 | "version": "0.4.4", 4 | "description": "The client-side scraping companion.", 5 | "main": "./build/artoo.node.js", 6 | "author": { 7 | "name": "Yomguithereal", 8 | "url": "https://github.com/Yomguithereal" 9 | }, 10 | "scripts": { 11 | "start": "gulp work", 12 | "https": "gulp https", 13 | "test": "gulp test", 14 | "prepublish": "gulp build" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "https://github.com/medialab/artoo" 19 | }, 20 | "keywords": [ 21 | "datamining", 22 | "scraping", 23 | "webscraper" 24 | ], 25 | "license": "MIT", 26 | "bugs": { 27 | "url": "https://github.com/medialab/artoo/issues" 28 | }, 29 | "homepage": "https://github.com/medialab/artoo", 30 | "devDependencies": { 31 | "gulp": "^4.0.2", 32 | "gulp-artoo": "~0.1.0", 33 | "gulp-concat": "^2.5.2", 34 | "gulp-header": "^1.7.1", 35 | "gulp-jshint": "^1.11.2", 36 | "gulp-mocha": "^2.0.0", 37 | "gulp-mocha-phantomjs": "^0.12.1", 38 | "gulp-rename": "~1.2.0", 39 | "gulp-replace": "~0.5.0", 40 | "gulp-uglify": "^1.1.0", 41 | "gulp-webserver": "^0.9.0", 42 | "phantomjs": "^2.1.7" 43 | }, 44 | "dependencies": { 45 | "cheerio": "^0.22.0" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/artoo.beep.js: -------------------------------------------------------------------------------- 1 | ;(function(undefined) { 2 | 'use strict'; 3 | 4 | /** 5 | * artoo beep 6 | * =========== 7 | * 8 | * Experimental feature designed to make artoo beep. 9 | */ 10 | 11 | var collections = { 12 | greet: ['announce', 'excited', 'hello', 'music', 'original', 'welcome'], 13 | info: ['determined', 'flourish', 'playful', 'sassy', 'talk', 'whistling'], 14 | warn: ['assert', 'laugh', 'question', 'quick', 'strange', 'threat'], 15 | error: ['sad', 'scream', 'shocked', 'weep'] 16 | }; 17 | 18 | var sounds = collections.greet 19 | .concat(collections.info) 20 | .concat(collections.warn) 21 | .concat(collections.error); 22 | 23 | // Helpers 24 | function randomInArray(a) { 25 | return a[Math.floor(Math.random() * a.length)]; 26 | } 27 | 28 | // Playing the base64 sound 29 | artoo.beep = function(a1, a2) { 30 | var sound, 31 | callback, 32 | chosenSound; 33 | 34 | if (typeof a1 === 'function') { 35 | callback = a1; 36 | } 37 | else { 38 | sound = a1; 39 | if (typeof a2 === 'function') 40 | callback = a2; 41 | else if (typeof a2 !== 'undefined') 42 | throw Error('artoo.beep: second argument has to be a function.'); 43 | } 44 | 45 | if (artoo.helpers.isArray(sound)) 46 | chosenSound = randomInArray(sound); 47 | else 48 | chosenSound = sound || randomInArray(sounds); 49 | 50 | if (chosenSound in collections) 51 | chosenSound = randomInArray(collections[chosenSound]); 52 | 53 | if (!~sounds.indexOf(chosenSound)) 54 | throw Error('artoo.beep: wrong sound specified.'); 55 | 56 | var player = new Audio(artoo.settings.beep.endpoint + chosenSound + '.ogg'); 57 | if(callback) 58 | player.addEventListener('ended', function() { 59 | callback(); 60 | }); 61 | player.play(); 62 | }; 63 | 64 | // Exposing available beeps 65 | Object.defineProperty(artoo.beep, 'available', { 66 | value: sounds 67 | }); 68 | 69 | // Exposing collections 70 | Object.defineProperty(artoo.beep, 'collections', { 71 | value: collections 72 | }); 73 | 74 | // Creating shortcuts 75 | // NOTE: not using bind here to avoid messing with phantomjs 76 | sounds.concat(Object.keys(collections)).forEach(function(s) { 77 | artoo.beep[s] = function() { 78 | artoo.beep(s); 79 | }; 80 | }); 81 | }).call(this); 82 | -------------------------------------------------------------------------------- /src/artoo.browser.js: -------------------------------------------------------------------------------- 1 | ;(function(undefined) { 2 | 'use strict'; 3 | 4 | /** 5 | * artoo browser module 6 | * ===================== 7 | * 8 | * Detects in which browser artoo is loaded and what are its capabilities 9 | * so he can adapt gracefully. 10 | */ 11 | var _root = this, 12 | inBrowser = 'navigator' in _root; 13 | 14 | // Helpers 15 | function checkFirebug() { 16 | var firebug = true; 17 | for (var i in _root.console.__proto__) { 18 | firebug = false; 19 | break; 20 | } 21 | return firebug; 22 | } 23 | 24 | function checkNode() { 25 | return typeof window === 'undefined' && 26 | typeof global !== 'undefined' && 27 | typeof module !== 'undefined' && 28 | module.exports; 29 | } 30 | 31 | // Browsers 32 | artoo.browser = { 33 | chrome: 'chrome' in _root, 34 | firefox: inBrowser && !!~navigator.userAgent.search(/firefox/i), 35 | phantomjs: 'callPhantom' in _root, 36 | nodejs: checkNode() 37 | }; 38 | 39 | // Which browser? 40 | artoo.browser.which = 41 | artoo.helpers.first(Object.keys(artoo.browser), function(b) { 42 | return artoo.browser[b]; 43 | }) || null; 44 | 45 | // Debuggers 46 | artoo.browser.firebug = checkFirebug(); 47 | }).call(this); 48 | -------------------------------------------------------------------------------- /src/artoo.countermeasures.js: -------------------------------------------------------------------------------- 1 | ;(function(undefined) { 2 | 'use strict'; 3 | 4 | /** 5 | * artoo countermeasures 6 | * ====================== 7 | * 8 | * Compilation of artoo countermeasures against popular console hacks 9 | * deployed by websites to prevent javasript fiddling. 10 | */ 11 | 12 | // Checking whether the console functions have been replaced by empty ones. 13 | // Examples: twitter, gmail 14 | function shuntedConsole() { 15 | 16 | // Detection 17 | if (artoo.browser.firebug || 18 | ~console.log.toString().search(/\[native code\]/i)) 19 | return; 20 | 21 | // The console have been shunted, repairing... 22 | ['log', 'info', 'debug', 'warn'].forEach(function(fn) { 23 | console[fn] = console.__proto__[fn]; 24 | }); 25 | 26 | artoo.log.warning('The console have been shunted by the website you ' + 27 | 'are visiting. artoo has repaired it.'); 28 | } 29 | 30 | // Registering functions 31 | artoo.once('countermeasures', function() { 32 | shuntedConsole(); 33 | }); 34 | }).call(this); 35 | -------------------------------------------------------------------------------- /src/artoo.dependencies.js: -------------------------------------------------------------------------------- 1 | ;(function(undefined) { 2 | 'use strict'; 3 | 4 | /** 5 | * artoo dependencies 6 | * =================== 7 | * 8 | * Gracefully inject popular dependencies into the scraped webpage. 9 | */ 10 | var _root = this, 11 | _cached = {}; 12 | 13 | artoo.deps = {}; 14 | 15 | // Dependencies injection routine 16 | // TODO: trust and function to check version and such 17 | artoo.deps._inject = function(cb) { 18 | var deps = artoo.settings.dependencies; 19 | 20 | if (!deps.length) 21 | return cb(); 22 | 23 | artoo.log.verbose( 24 | 'Starting to retrieve dependencies...', 25 | deps.map(function(d) { 26 | return d.name; 27 | }) 28 | ); 29 | 30 | // Creating tasks 31 | var tasks = deps.map(function(d) { 32 | if (!d.name || !d.globals || !d.url) 33 | throw Error('artoo.deps: invalid dependency definition.'); 34 | 35 | // Computing globals 36 | var globals = typeof d.globals === 'string' ? [d.globals] : d.globals; 37 | globals.forEach(function(g) { 38 | 39 | // Is the variable present in the global scope? 40 | if (_root[g] && !d.noConflict && !d.force) 41 | _cached[g] = _root[g]; 42 | }); 43 | 44 | // Creating a task 45 | return function(next) { 46 | 47 | // Script injection 48 | artoo.injectScript(d.url, function() { 49 | 50 | // Announcing 51 | artoo.log.verbose('Retrieved dependency ' + d.name + '.'); 52 | 53 | // Retrieving the variables under artoo.deps 54 | var retrievedGlobals = {}; 55 | globals.forEach(function(g) { 56 | 57 | retrievedGlobals[g] = _root[g]; 58 | 59 | // If cached and not forced 60 | if (_cached[g]) { 61 | _root[g] = _cached[g]; 62 | delete _cached[g]; 63 | } 64 | 65 | // If noConflict and not forced 66 | if (d.noConflict) 67 | _root[g].noConflict(); 68 | }); 69 | 70 | // Assigning to deps 71 | artoo.deps[d.name] = Object.keys(retrievedGlobals).length > 1 ? 72 | retrievedGlobals : 73 | retrievedGlobals[Object.keys(retrievedGlobals)[0]]; 74 | 75 | next(); 76 | }); 77 | }; 78 | }); 79 | 80 | artoo.helpers.parallel(tasks, function() { 81 | artoo.log.verbose('Finished retrieving dependencies.'); 82 | cb(); 83 | }); 84 | }; 85 | 86 | // jQuery injection routine 87 | artoo.jquery.inject = function(cb) { 88 | 89 | // Properties 90 | var desiredVersion = artoo.settings.jquery.version, 91 | cdn = '//code.jquery.com/jquery-' + desiredVersion + '.min.js'; 92 | 93 | // Checking the existence of jQuery or of another library. 94 | var exists = (typeof jQuery !== 'undefined' && jQuery.fn) || artoo.$.fn, 95 | other = !exists && typeof $ !== 'undefined', 96 | currentVersion = exists && jQuery.fn.jquery ? jQuery.fn.jquery : '0'; 97 | 98 | // jQuery is already in a correct mood 99 | if (exists && 100 | currentVersion.charAt(0) === desiredVersion.charAt(0) && 101 | currentVersion.charAt(2) === desiredVersion.charAt(2)) { 102 | artoo.log.verbose('jQuery already exists in this page ' + 103 | '(v' + currentVersion + '). No need to load it again.'); 104 | 105 | // Internal reference 106 | artoo.$ = jQuery; 107 | 108 | cb(); 109 | } 110 | 111 | // Forcing jQuery injection, according to settings 112 | else if (artoo.settings.jquery.force) { 113 | artoo.injectScript(cdn, function() { 114 | artoo.log.warning('According to your settings, jQuery (v' + 115 | desiredVersion + ') was injected into your page ' + 116 | 'to replace the current $ variable.'); 117 | 118 | artoo.$ = jQuery; 119 | 120 | cb(); 121 | }); 122 | } 123 | 124 | // jQuery has not the correct version or another library uses $ 125 | else if ((exists && currentVersion.charAt(0) !== '2') || other) { 126 | artoo.injectScript(cdn, function() { 127 | artoo.$ = jQuery.noConflict(true); 128 | 129 | // Then, if dollar does not exist, we set it 130 | if (typeof _root.$ === 'undefined') { 131 | _root.$ = artoo.$; 132 | 133 | artoo.log.warning( 134 | 'jQuery is available but does not have a correct version. ' + 135 | 'The correct version was therefore injected and $ was set since ' + 136 | 'it was not used.' 137 | ); 138 | } 139 | else { 140 | artoo.log.warning( 141 | 'Either jQuery has not a valid version or another library ' + 142 | 'using $ is already present. ' + 143 | 'Correct version available through `artoo.$`.' 144 | ); 145 | } 146 | 147 | cb(); 148 | }); 149 | } 150 | 151 | // jQuery does not exist at all, we load it 152 | else { 153 | artoo.injectScript(cdn, function() { 154 | artoo.log.info('jQuery was correctly injected into your page ' + 155 | '(v' + desiredVersion + ').'); 156 | 157 | artoo.$ = jQuery; 158 | 159 | cb(); 160 | }); 161 | } 162 | }; 163 | }).call(this); 164 | -------------------------------------------------------------------------------- /src/artoo.helpers.js: -------------------------------------------------------------------------------- 1 | ;(function(undefined) { 2 | 'use strict'; 3 | 4 | /** 5 | * artoo helpers 6 | * ============== 7 | * 8 | * Some useful helpers. 9 | */ 10 | var _root = this; 11 | 12 | // Extending Emmett 13 | Object.setPrototypeOf = Object.setPrototypeOf || function (obj, proto) { 14 | obj.__proto__ = proto; 15 | return obj; 16 | }; 17 | var ee = new artoo.emitter(); 18 | Object.setPrototypeOf(artoo, Object.getPrototypeOf(ee)); 19 | 20 | 21 | // Legacy support 22 | // TODO: drop this asap 23 | artoo.hooks = { 24 | trigger: function(name) { 25 | artoo.emit(name); 26 | } 27 | }; 28 | 29 | 30 | /** 31 | * Generic Helpers 32 | * ---------------- 33 | * 34 | * Some basic helpers from collection handling to type checking. 35 | */ 36 | 37 | // Useless function 38 | function noop() {} 39 | 40 | // Recursively extend objects 41 | function extend() { 42 | var i, 43 | k, 44 | res = {}, 45 | l = arguments.length; 46 | 47 | for (i = l - 1; i >= 0; i--) 48 | for (k in arguments[i]) 49 | if (res[k] && isPlainObject(arguments[i][k])) 50 | res[k] = extend(arguments[i][k], res[k]); 51 | else 52 | res[k] = arguments[i][k]; 53 | 54 | return res; 55 | } 56 | 57 | // Is the var an array? 58 | function isArray(v) { 59 | return v instanceof Array; 60 | } 61 | 62 | // Is the var an object? 63 | function isObject(v) { 64 | return v instanceof Object; 65 | } 66 | 67 | // Is the var a real NaN 68 | function isRealNaN(v) { 69 | return isNaN(v) && (typeof v === 'number'); 70 | } 71 | 72 | // Is the var a plain object? 73 | function isPlainObject(v) { 74 | return v instanceof Object && 75 | !(v instanceof Array) && 76 | !(v instanceof Function); 77 | } 78 | 79 | // Is a var non primitive? 80 | function isNonPrimitive(v) { 81 | return isPlainObject(v) || isArray(v); 82 | } 83 | 84 | // Is a var primitive? 85 | function isPrimitive(v) { 86 | return !isNonScalar(v); 87 | } 88 | 89 | // Get first item of array returning true to given function 90 | function first(a, fn, scope) { 91 | for (var i = 0, l = a.length; i < l; i++) { 92 | if (fn.call(scope || null, a[i])) 93 | return a[i]; 94 | } 95 | return; 96 | } 97 | 98 | // Get the index of an element in an array by function 99 | function indexOf(a, fn, scope) { 100 | for (var i = 0, l = a.length; i < l; i++) { 101 | if (fn.call(scope || null, a[i])) 102 | return i; 103 | } 104 | return -1; 105 | } 106 | 107 | // Retrieve a file extenstion from filename or url 108 | function getExtension(url) { 109 | var a = url.split('.'); 110 | 111 | if (a.length === 1 || (a[0] === '' && a.length === 2)) 112 | return ''; 113 | return a.pop(); 114 | } 115 | 116 | /** 117 | * Document Helpers 118 | * ----------------- 119 | * 120 | * Functions to deal with DOM selection and the current document. 121 | */ 122 | 123 | // Checking whether a variable is a jQuery selector 124 | function isSelector(v) { 125 | return (artoo.$ && v instanceof artoo.$) || 126 | (jQuery && v instanceof jQuery) || 127 | ($ && v instanceof $); 128 | } 129 | 130 | // Checking whether a variable is a DOM document 131 | function isDocument(v) { 132 | return v instanceof HTMLDocument || 133 | v instanceof XMLDocument; 134 | } 135 | 136 | // Get either string or document and return valid jQuery selection 137 | function jquerify(v) { 138 | var $ = artoo.$; 139 | 140 | if (isDocument(v)) 141 | return $(v); 142 | return $('
').append(v); 143 | } 144 | 145 | // Creating an HTML or XML document 146 | function createDocument(root, namespace) { 147 | if (!root) 148 | return document.implementation.createHTMLDocument(); 149 | else 150 | return document.implementation.createDocument( 151 | namespace || null, 152 | root, 153 | null 154 | ); 155 | } 156 | 157 | // Loading an external file the same way the browser would load it from page 158 | function getScript(url, async, cb) { 159 | if (typeof async === 'function') { 160 | cb = async; 161 | async = false; 162 | } 163 | 164 | var el = document.createElement('script'); 165 | 166 | // Script attributes 167 | el.type = 'text/javascript'; 168 | el.src = url; 169 | 170 | // Should the script be loaded asynchronously? 171 | if (async) 172 | el.async = true; 173 | 174 | // Defining callbacks 175 | el.onload = el.onreadystatechange = function() { 176 | if ((!this.readyState || 177 | this.readyState == 'loaded' || 178 | this.readyState == 'complete')) { 179 | el.onload = el.onreadystatechange = null; 180 | 181 | // Removing element from head 182 | artoo.mountNode.removeChild(el); 183 | 184 | if (typeof cb === 'function') 185 | cb(); 186 | } 187 | }; 188 | 189 | // Appending the script to head 190 | artoo.mountNode.appendChild(el); 191 | } 192 | 193 | // Loading an external stylesheet 194 | function getStylesheet(data, isUrl, cb) { 195 | var el = document.createElement(isUrl ? 'link' : 'style'), 196 | head = document.getElementsByTagName('head')[0]; 197 | 198 | el.type = 'text/css'; 199 | 200 | if (isUrl) { 201 | el.href = data; 202 | el.rel = 'stylesheet'; 203 | 204 | // Waiting for script to load 205 | el.onload = el.onreadystatechange = function() { 206 | if ((!this.readyState || 207 | this.readyState == 'loaded' || 208 | this.readyState == 'complete')) { 209 | el.onload = el.onreadystatechange = null; 210 | 211 | if (typeof cb === 'function') 212 | cb(); 213 | } 214 | }; 215 | } 216 | else { 217 | el.innerHTML = data; 218 | } 219 | 220 | // Appending the stylesheet to head 221 | head.appendChild(el); 222 | } 223 | 224 | var globalsBlackList = [ 225 | '__commandLineAPI', 226 | 'applicationCache', 227 | 'chrome', 228 | 'closed', 229 | 'console', 230 | 'crypto', 231 | 'CSS', 232 | 'defaultstatus', 233 | 'defaultStatus', 234 | 'devicePixelRatio', 235 | 'document', 236 | 'external', 237 | 'frameElement', 238 | 'history', 239 | 'indexedDB', 240 | 'innerHeight', 241 | 'innerWidth', 242 | 'length', 243 | 'localStorage', 244 | 'location', 245 | 'name', 246 | 'offscreenBuffering', 247 | 'opener', 248 | 'outerHeight', 249 | 'outerWidth', 250 | 'pageXOffset', 251 | 'pageYOffset', 252 | 'performance', 253 | 'screen', 254 | 'screenLeft', 255 | 'screenTop', 256 | 'screenX', 257 | 'screenY', 258 | 'scrollX', 259 | 'scrollY', 260 | 'sessionStorage', 261 | 'speechSynthesis', 262 | 'status', 263 | 'styleMedia' 264 | ]; 265 | 266 | function getGlobalVariables() { 267 | var p = Object.getPrototypeOf(_root), 268 | o = {}, 269 | i; 270 | 271 | for (i in _root) 272 | if (!~i.indexOf('webkit') && 273 | !(i in p) && 274 | _root[i] !== _root && 275 | !(_root[i] instanceof BarProp) && 276 | !(_root[i] instanceof Navigator) && 277 | !~globalsBlackList.indexOf(i)) 278 | o[i] = _root[i]; 279 | 280 | return o; 281 | } 282 | 283 | 284 | /** 285 | * Async Helpers 286 | * -------------- 287 | * 288 | * Some helpful functions to deal with asynchronous matters. 289 | */ 290 | 291 | // Waiting for something to happen 292 | function waitFor(check, cb, params) { 293 | params = params || {}; 294 | if (typeof cb === 'object') { 295 | params = cb; 296 | cb = params.done; 297 | } 298 | 299 | var milliseconds = params.interval || 30, 300 | j = 0; 301 | 302 | var i = setInterval(function() { 303 | if (check()) { 304 | clearInterval(i); 305 | cb(null); 306 | } 307 | 308 | if (params.timeout && params.timeout - (j * milliseconds) <= 0) { 309 | clearInterval(i); 310 | cb(new Error('timeout')); 311 | } 312 | 313 | j++; 314 | }, milliseconds); 315 | } 316 | 317 | // Dispatch asynchronous function 318 | function async() { 319 | var args = Array.prototype.slice.call(arguments); 320 | return setTimeout.apply(null, [args[0], 0].concat(args.slice(1))); 321 | } 322 | 323 | // Launching tasks in parallel with an optional limit 324 | function parallel(tasks, params, last) { 325 | var onEnd = (typeof params === 'function') ? params : params.done || last, 326 | running = [], 327 | results = [], 328 | d = 0, 329 | t, 330 | l, 331 | i; 332 | 333 | if (typeof onEnd !== 'function') 334 | onEnd = noop; 335 | 336 | function cleanup() { 337 | running.forEach(function(r) { 338 | clearTimeout(r); 339 | }); 340 | } 341 | 342 | function onTaskEnd(err, result) { 343 | // Adding results to accumulator 344 | results.push(result); 345 | 346 | if (err) { 347 | cleanup(); 348 | return onEnd(err, results); 349 | } 350 | 351 | if (++d >= tasks.length) { 352 | 353 | // Parallel action is finished, returning 354 | return onEnd(null, results); 355 | } 356 | 357 | // Adding on stack 358 | t = tasks[i++]; 359 | running.push(async(t, onTaskEnd)); 360 | } 361 | 362 | for (i = 0, l = params.limit || tasks.length; i < l; i++) { 363 | t = tasks[i]; 364 | 365 | // Dispatching the function asynchronously 366 | running.push(async(t, onTaskEnd)); 367 | } 368 | } 369 | 370 | 371 | /** 372 | * Monkey Patching 373 | * ---------------- 374 | * 375 | * Some monkey patching shortcuts. Useful for sniffers and overriding 376 | * native functions. 377 | */ 378 | 379 | function before(targetFunction, beforeFunction) { 380 | 381 | // Replacing the target function 382 | return function() { 383 | 384 | // Applying our function 385 | beforeFunction.apply(this, Array.prototype.slice.call(arguments)); 386 | 387 | // Applying the original function 388 | return targetFunction.apply(this, Array.prototype.slice.call(arguments)); 389 | }; 390 | } 391 | 392 | 393 | /** 394 | * Exportation 395 | * ------------ 396 | */ 397 | 398 | // Exporting to artoo root 399 | artoo.injectScript = function(url, cb) { 400 | getScript(url, cb); 401 | }; 402 | artoo.injectStyle = function(url, cb) { 403 | getStylesheet(url, true, cb); 404 | }; 405 | artoo.injectInlineStyle = function(text) { 406 | getStylesheet(text, false); 407 | }; 408 | artoo.waitFor = waitFor; 409 | artoo.getGlobalVariables = getGlobalVariables; 410 | 411 | // Exporting to artoo helpers 412 | artoo.helpers = { 413 | before: before, 414 | createDocument: createDocument, 415 | extend: extend, 416 | first: first, 417 | getExtension: getExtension, 418 | indexOf: indexOf, 419 | isArray: isArray, 420 | isDocument: isDocument, 421 | isObject: isObject, 422 | isPlainObject: isPlainObject, 423 | isRealNaN: isRealNaN, 424 | isSelector: isSelector, 425 | isNonPrimitive: isNonPrimitive, 426 | isPrimitive: isPrimitive, 427 | jquerify: jquerify, 428 | noop: noop, 429 | parallel: parallel 430 | }; 431 | }).call(this); 432 | -------------------------------------------------------------------------------- /src/artoo.init.js: -------------------------------------------------------------------------------- 1 | ;(function(undefined) { 2 | 'use strict'; 3 | 4 | /** 5 | * artoo initialization 6 | * ===================== 7 | * 8 | * artoo's inititialization routine. 9 | */ 10 | var _root = this; 11 | 12 | // Script evaluation function 13 | var firstExec = true; 14 | function exec() { 15 | 16 | // Should we reExec? 17 | if (!artoo.settings.reExec && !firstExec) { 18 | artoo.log.warning('not reexecuting script as per settings.'); 19 | return; 20 | } 21 | 22 | // Evaluating or invoking distant script? 23 | if (artoo.settings.eval) { 24 | artoo.log.verbose('evaluating and executing the script given to artoo.'); 25 | eval.call(_root, JSON.parse(artoo.settings.eval)); 26 | } 27 | else if (artoo.settings.scriptUrl) { 28 | artoo.log.verbose('executing script at "' + 29 | artoo.settings.scriptUrl + '"'); 30 | artoo.injectScript(artoo.settings.scriptUrl); 31 | } 32 | 33 | firstExec = false; 34 | } 35 | 36 | // Initialization function 37 | function main() { 38 | 39 | // Triggering countermeasures 40 | artoo.emit('countermeasures'); 41 | 42 | // Welcoming user 43 | if (artoo.settings.log.welcome) 44 | artoo.log.welcome(); 45 | 46 | // Should we greet the user with a joyful beep? 47 | var beeping = artoo.settings.log.beeping; 48 | if (beeping) 49 | artoo.beep.greet(); 50 | 51 | // Indicating we are injecting artoo from the chrome extension 52 | if (artoo.browser.chromeExtension) 53 | artoo.log.verbose('artoo has automatically been injected ' + 54 | 'by the chrome extension.'); 55 | 56 | // If in phantom, dependencies are loaded synchronously 57 | if (artoo.browser.phantomjs) { 58 | artoo.$ = window.artooPhantomJQuery; 59 | delete window.artooPhantomJQuery; 60 | artoo.jquery.applyPlugins(); 61 | return artoo.emit('ready'); 62 | } 63 | 64 | 65 | // Injecting dependencies 66 | function injectJquery(cb) { 67 | artoo.jquery.inject(function() { 68 | 69 | // Applying jQuery plugins 70 | artoo.jquery.applyPlugins(); 71 | 72 | cb(); 73 | }); 74 | } 75 | 76 | artoo.helpers.parallel( 77 | [injectJquery, artoo.deps._inject], 78 | function() { 79 | artoo.log.info('artoo is now good to go!'); 80 | 81 | // Triggering exec 82 | if (artoo.settings.autoExec) 83 | artoo.exec(); 84 | 85 | // Triggering ready 86 | artoo.emit('ready'); 87 | } 88 | ); 89 | 90 | // Updating artoo state 91 | artoo.loaded = true; 92 | } 93 | 94 | // Retrieving settings from script tag 95 | var dom = document.getElementById('artoo_injected_script'); 96 | 97 | if (dom) { 98 | artoo.loadSettings(JSON.parse(dom.getAttribute('settings'))); 99 | dom.parentNode.removeChild(dom); 100 | } 101 | 102 | // Updating artoo.browser 103 | artoo.browser.chromeExtension = !!artoo.settings.chromeExtension; 104 | 105 | // Adding functions to hooks 106 | artoo.once('init', main); 107 | artoo.on('exec', exec); 108 | 109 | // artoo initialization 110 | artoo.init = function() { 111 | artoo.emit('init'); 112 | }; 113 | 114 | // artoo exectution 115 | artoo.exec = function() { 116 | artoo.emit('exec'); 117 | }; 118 | 119 | // Init? 120 | if (artoo.settings.autoInit) 121 | artoo.init(); 122 | }).call(this); 123 | -------------------------------------------------------------------------------- /src/artoo.js: -------------------------------------------------------------------------------- 1 | ;(function(undefined) { 2 | 'use strict'; 3 | 4 | /** 5 | * artoo core 6 | * =========== 7 | * 8 | * The main artoo namespace and its vital properties. 9 | */ 10 | 11 | // Checking whether a body exists 12 | var body; 13 | if ('document' in this) { 14 | body = document.getElementsByTagName('body')[0]; 15 | if (!body) { 16 | body = document.createElement('body'); 17 | document.documentElement.appendChild(body); 18 | } 19 | } 20 | 21 | // Main object 22 | var artoo = { 23 | 24 | // Standard properties 25 | $: {}, 26 | jquery: { 27 | applyPlugins: function() { 28 | artoo.jquery.plugins.map(function(p) { 29 | p(artoo.$); 30 | }); 31 | }, 32 | plugins: [] 33 | }, 34 | mountNode: body, 35 | stylesheets: {}, 36 | templates: {}, 37 | 38 | // Emitter shim properties 39 | _enabled: true, 40 | _children: [], 41 | _handlers: {}, 42 | _handlersAll: [] 43 | }; 44 | 45 | // Non-writable version 46 | Object.defineProperty(artoo, 'version', { 47 | value: '0.4.4' 48 | }); 49 | 50 | // Exporting to global scope 51 | this.artoo = artoo; 52 | }).call(this); 53 | -------------------------------------------------------------------------------- /src/artoo.log.js: -------------------------------------------------------------------------------- 1 | ;(function(undefined) { 2 | 'use strict'; 3 | 4 | /** 5 | * artoo console abstraction 6 | * ========================== 7 | * 8 | * Console abstraction enabling artoo to perform a finer logging job than 9 | * standard one. 10 | */ 11 | var _root = this, 12 | enhanced = artoo.browser.chrome || artoo.browser.firebug; 13 | 14 | // Log levels 15 | var levels = { 16 | verbose: '#33CCFF', // Cyan 17 | debug: '#000099', // Blue 18 | info: '#009900', // Green 19 | warning: 'orange', // Orange 20 | error: 'red' // Red 21 | }; 22 | 23 | var priorities = ['verbose', 'debug', 'info', 'warning', 'error']; 24 | 25 | // Utilities 26 | function toArray(a, slice) { 27 | return Array.prototype.slice.call(a, slice || 0); 28 | } 29 | 30 | // Is the level allowed to log? 31 | function isAllowed(level) { 32 | var threshold = artoo.settings.log.level; 33 | 34 | if (artoo.helpers.isArray(threshold)) 35 | return !!~threshold.indexOf(level); 36 | else 37 | return priorities.indexOf(level) >= 38 | priorities.indexOf(threshold); 39 | } 40 | 41 | // Return the logo ASCII array 42 | function robot() { 43 | return [ 44 | ' .-""-. ', 45 | ' /[] _ _\\ ', 46 | ' _|_o_LII|_ ', 47 | '/ | ==== | \\', 48 | '|_| ==== |_|', 49 | ' ||LI o ||', 50 | ' ||\'----\'||', 51 | '/__| |__\\' 52 | ]; 53 | } 54 | 55 | // Log header 56 | function logHeader(level) { 57 | var args = ['[artoo]: ' + (enhanced ? '%c' + level : '')]; 58 | 59 | if (enhanced) 60 | args.push('color: ' + levels[level] + ';'); 61 | args.push('-' + (enhanced ? '' : ' ')); 62 | 63 | return args; 64 | } 65 | 66 | // Log override 67 | artoo.log = function(level) { 68 | if (!artoo.settings.log.enabled) 69 | return; 70 | 71 | var hasLevel = (levels[level] !== undefined), 72 | slice = hasLevel ? 1 : 0, 73 | args = toArray(arguments, slice); 74 | 75 | level = hasLevel ? level : 'debug'; 76 | 77 | // Is this level allowed? 78 | if (!isAllowed(level)) 79 | return; 80 | 81 | var msg = logHeader(level).concat(args); 82 | 83 | console.log.apply( 84 | console, 85 | (enhanced) ? 86 | msg : 87 | [msg.reduce(function(a, b) { return a + b; }, '')] 88 | ); 89 | }; 90 | 91 | // Log shortcuts 92 | function makeShortcut(level) { 93 | artoo.log[level] = function() { 94 | artoo.log.apply(artoo.log, 95 | [level].concat(toArray(arguments))); 96 | }; 97 | } 98 | 99 | for (var l in levels) 100 | makeShortcut(l); 101 | 102 | // Plain log 103 | artoo.log.plain = function() { 104 | if (artoo.settings.log.enabled) 105 | console.log.apply(console, arguments); 106 | }; 107 | 108 | // Logo display 109 | artoo.log.welcome = function() { 110 | if (!artoo.settings.log.enabled) 111 | return; 112 | 113 | var ascii = robot(); 114 | ascii[ascii.length - 2] = ascii[ascii.length - 2] + ' artoo.js'; 115 | 116 | console.log(ascii.join('\n') + ' v' + artoo.version); 117 | }; 118 | }).call(this); 119 | -------------------------------------------------------------------------------- /src/artoo.parsers.js: -------------------------------------------------------------------------------- 1 | ;(function(undefined) { 2 | 'use strict'; 3 | 4 | /** 5 | * artoo parsers 6 | * ============== 7 | * 8 | * Compilation of small parsers aim at understanding some popular web 9 | * string formats such as querystrings, headers etc. 10 | */ 11 | 12 | function parseQueryString(s) { 13 | var data = {}; 14 | 15 | s.split('&').forEach(function(item) { 16 | var pair = item.split('='); 17 | data[decodeURIComponent(pair[0])] = 18 | pair[1] ? decodeURIComponent(pair[1]) : true; 19 | }); 20 | 21 | return data; 22 | } 23 | 24 | function parseUrl(url) { 25 | var data = {href: url}; 26 | 27 | // Searching for a protocol 28 | var ps = url.split('://'); 29 | 30 | if (ps.length > 1) 31 | data.protocol = ps[0]; 32 | 33 | url = ps[ps.length > 1 ? 1 : 0]; 34 | 35 | // Searching for an authentification 36 | var a = url.split('@'); 37 | if (a.length > 1) { 38 | var as = a[0].split(':'); 39 | if (as.length > 1) { 40 | data.auth = { 41 | user: as[0], 42 | password: as[1] 43 | }; 44 | } 45 | else { 46 | data.auth = { 47 | user: as[0] 48 | }; 49 | } 50 | 51 | url = a[1]; 52 | } 53 | 54 | // Searching for origin 55 | var m = url.match(/([^\/:]+)(.*)/); 56 | data.host = m[1]; 57 | data.hostname = m[1]; 58 | 59 | if (m[2]) { 60 | var f = m[2].trim(); 61 | 62 | // Port 63 | if (f.charAt(0) === ':') { 64 | data.port = +f.match(/\d+/)[0]; 65 | data.host += ':' + data.port; 66 | } 67 | 68 | // Path 69 | data.path = '/' + f.split('/').slice(1).join('/'); 70 | 71 | data.pathname = data.path.split('?')[0].split('#')[0]; 72 | } 73 | 74 | // Tld 75 | if (~data.hostname.search('.')) { 76 | var ds = data.hostname.split('.'); 77 | 78 | // Check for IP 79 | if (!(ds.length === 4 && 80 | ds.every(function(i) { return !isNaN(+i); }))) { 81 | 82 | // Checking TLD-less urls 83 | if (ds.length > 1) { 84 | 85 | // TLD 86 | data.tld = ds[ds.length - 1]; 87 | 88 | // Domain 89 | data.domain = ds[ds.length - 2]; 90 | 91 | // Subdomains 92 | if (ds.length > 2) { 93 | data.subdomains = []; 94 | for (var i = 0, l = ds.length - 2; i < l; i++) 95 | data.subdomains.unshift(ds[i]); 96 | } 97 | } 98 | else { 99 | 100 | // TLD-less url 101 | data.domain = ds[0]; 102 | } 103 | } 104 | else { 105 | 106 | // This is an IP 107 | data.domain = data.hostname; 108 | } 109 | } 110 | 111 | // Hash 112 | var hs = url.split('#'); 113 | 114 | if (hs.length > 1) { 115 | data.hash = '#' + hs[1]; 116 | } 117 | 118 | // Querystring 119 | var qs = url.split('?'); 120 | 121 | if (qs.length > 1) { 122 | data.search = '?' + qs[1]; 123 | data.query = parseQueryString(qs[1]); 124 | } 125 | 126 | // Extension 127 | var ss = data.pathname.split('/'), 128 | es = ss[ss.length - 1].split('.'); 129 | 130 | if (es.length > 1) 131 | data.extension = es[es.length - 1]; 132 | 133 | return data; 134 | } 135 | 136 | function parseHeaders(headers) { 137 | var data = {}; 138 | 139 | headers.split('\n').filter(function(item) { 140 | return item.trim(); 141 | }).forEach(function(item) { 142 | if (item) { 143 | var pair = item.split(': '); 144 | data[pair[0]] = pair[1]; 145 | } 146 | }); 147 | 148 | return data; 149 | } 150 | 151 | function parseCookie(s) { 152 | var cookie = { 153 | httpOnly: false, 154 | secure: false 155 | }; 156 | 157 | if (!s.trim()) 158 | return; 159 | 160 | s.split('; ').forEach(function(item) { 161 | 162 | // Path 163 | if (~item.search(/path=/i)) { 164 | cookie.path = item.split('=')[1]; 165 | } 166 | else if (~item.search(/expires=/i)) { 167 | cookie.expires = item.split('=')[1]; 168 | } 169 | else if (~item.search(/httponly/i) && !~item.search('=')) { 170 | cookie.httpOnly = true; 171 | } 172 | else if (~item.search(/secure/i) && !~item.search('=')) { 173 | cookie.secure = true; 174 | } 175 | else { 176 | var is = item.split('='); 177 | cookie.key = is[0]; 178 | cookie.value = decodeURIComponent(is[1]); 179 | } 180 | }); 181 | 182 | return cookie; 183 | } 184 | 185 | function parseCookies(s) { 186 | var cookies = {}; 187 | 188 | if (!s.trim()) 189 | return cookies; 190 | 191 | s.split('; ').forEach(function(item) { 192 | var pair = item.split('='); 193 | cookies[pair[0]] = decodeURIComponent(pair[1]); 194 | }); 195 | 196 | return cookies; 197 | } 198 | 199 | /** 200 | * Exporting 201 | */ 202 | artoo.parsers = { 203 | cookie: parseCookie, 204 | cookies: parseCookies, 205 | headers: parseHeaders, 206 | queryString: parseQueryString, 207 | url: parseUrl 208 | }; 209 | }).call(this); 210 | -------------------------------------------------------------------------------- /src/artoo.settings.js: -------------------------------------------------------------------------------- 1 | ;(function(undefined) { 2 | 'use strict'; 3 | 4 | /** 5 | * artoo settings 6 | * =============== 7 | * 8 | * artoo default settings that user may override. 9 | */ 10 | 11 | // Defaults 12 | artoo.settings = { 13 | 14 | // Root settings 15 | autoInit: true, 16 | autoExec: true, 17 | chromeExtension: false, 18 | env: 'dev', 19 | eval: null, 20 | reExec: true, 21 | reload: false, 22 | scriptUrl: null, 23 | 24 | // Methods settings 25 | beep: { 26 | endpoint: '//medialab.github.io/artoo/sounds/' 27 | }, 28 | cache: { 29 | delimiter: '%' 30 | }, 31 | dependencies: [], 32 | jquery: { 33 | version: '2.1.3', 34 | force: false 35 | }, 36 | log: { 37 | beeping: false, 38 | enabled: true, 39 | level: 'verbose', 40 | welcome: true 41 | }, 42 | store: { 43 | engine: 'local' 44 | } 45 | }; 46 | 47 | // Setting utility 48 | artoo.loadSettings = function(ns) { 49 | artoo.settings = artoo.helpers.extend(ns, artoo.settings); 50 | }; 51 | }).call(this); 52 | -------------------------------------------------------------------------------- /src/artoo.writers.js: -------------------------------------------------------------------------------- 1 | ;(function(undefined) { 2 | 'use strict'; 3 | 4 | /** 5 | * artoo writers 6 | * ============== 7 | * 8 | * Compilation of writers for popular formats such as CSV or YAML. 9 | */ 10 | 11 | // Dependencies 12 | var isPlainObject = artoo.helpers.isPlainObject, 13 | isArray = artoo.helpers.isArray, 14 | isPrimitive = artoo.helpers.isPrimitive, 15 | isNonPrimitive = artoo.helpers.isNonPrimitive, 16 | isRealNaN = artoo.helpers.isRealNaN; 17 | 18 | 19 | /** 20 | * CSV 21 | * --- 22 | * 23 | * Converts an array of array or array of objects into a correct 24 | * CSV string for exports purposes. 25 | * 26 | * Exposes some handful options such as choice of delimiters or order 27 | * of keys to handle. 28 | */ 29 | 30 | // Convert an object into an array of its properties 31 | function objectToArray(o, order) { 32 | order = order || Object.keys(o); 33 | 34 | return order.map(function(k) { 35 | return o[k]; 36 | }); 37 | } 38 | 39 | // Retrieve an index of keys present in an array of objects 40 | function keysIndex(a) { 41 | var keys = [], 42 | l, 43 | k, 44 | i; 45 | 46 | for (i = 0, l = a.length; i < l; i++) 47 | for (k in a[i]) 48 | if (!~keys.indexOf(k)) 49 | keys.push(k); 50 | 51 | return keys; 52 | } 53 | 54 | // Escape a string for a RegEx 55 | function rescape(s) { 56 | return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); 57 | } 58 | 59 | // Converting an array of arrays into a CSV string 60 | function toCSVString(data, params) { 61 | if (data.length === 0) { 62 | return ''; 63 | } 64 | params = params || {}; 65 | 66 | var header = params.headers || [], 67 | plainObject = isPlainObject(data[0]), 68 | keys = plainObject && (params.order || keysIndex(data)), 69 | oData, 70 | i; 71 | 72 | // Defaults 73 | var escape = params.escape || '"', 74 | delimiter = params.delimiter || ','; 75 | 76 | // Dealing with headers polymorphism 77 | if (!header.length) 78 | if (plainObject && params.headers !== false) 79 | header = keys; 80 | 81 | // Should we append headers 82 | oData = (header.length ? [header] : []).concat( 83 | plainObject ? 84 | data.map(function(e) { return objectToArray(e, keys); }) : 85 | data 86 | ); 87 | 88 | // Converting to string 89 | return oData.map(function(row) { 90 | return row.map(function(item) { 91 | 92 | // Wrapping escaping characters 93 | var i = ('' + (typeof item === 'undefined' ? '' : item)).replace( 94 | new RegExp(rescape(escape), 'g'), 95 | escape + escape 96 | ); 97 | 98 | // Escaping if needed 99 | return ~i.indexOf(delimiter) || ~i.indexOf(escape) || ~i.indexOf('\n') ? 100 | escape + i + escape : 101 | i; 102 | }).join(delimiter); 103 | }).join('\n'); 104 | } 105 | 106 | 107 | 108 | /** 109 | * YAML 110 | * ---- 111 | * 112 | * Converts JavaScript data into a YAML string for export purposes. 113 | */ 114 | 115 | // Characters to escape in YAML 116 | var ymlEscape = /[:#,\-\[\]\{\}&%]|!{1,2}/; 117 | 118 | // Creating repeating sequences 119 | function repeatString(string, nb) { 120 | var s = string, 121 | l, 122 | i; 123 | 124 | if (nb <= 0) 125 | return ''; 126 | 127 | for (i = 1, l = nb | 0; i < l; i++) 128 | s += string; 129 | return s; 130 | } 131 | 132 | // YAML conversion 133 | var yml = { 134 | string: function(string) { 135 | return (~string.search(ymlEscape)) ? 136 | '\'' + string.replace(/'/g, '\'\'') + '\'' : 137 | string; 138 | }, 139 | number: function(nb) { 140 | return '' + nb; 141 | }, 142 | array: function(a, lvl) { 143 | lvl = lvl || 0; 144 | 145 | if (!a.length) 146 | return '[]'; 147 | 148 | var string = '', 149 | l, 150 | i; 151 | 152 | for (i = 0, l = a.length; i < l; i++) { 153 | string += repeatString(' ', lvl); 154 | 155 | if (isPrimitive(a[i])) { 156 | string += '- ' + processYAMLVariable(a[i]) + '\n'; 157 | } 158 | else { 159 | if (isPlainObject(a[i])) 160 | string += '-' + processYAMLVariable(a[i], lvl + 1, true); 161 | else 162 | string += processYAMLVariable(a[i], lvl + 1); 163 | } 164 | } 165 | 166 | return string; 167 | }, 168 | object: function(o, lvl, indent) { 169 | lvl = lvl || 0; 170 | 171 | if (!Object.keys(o).length) 172 | return (lvl ? '- ' : '') + '{}'; 173 | 174 | var string = '', 175 | key, 176 | c = 0, 177 | i; 178 | 179 | for (i in o) { 180 | key = yml.string(i); 181 | string += repeatString(' ', lvl); 182 | if (indent && !c) 183 | string = string.slice(0, -1); 184 | string += key + ': ' + (isNonPrimitive(o[i]) ? '\n' : '') + 185 | processYAMLVariable(o[i], lvl + 1) + '\n'; 186 | 187 | c++; 188 | } 189 | 190 | return string; 191 | }, 192 | fn: function(fn) { 193 | return yml.string(fn.toString()); 194 | }, 195 | boolean: function(v) { 196 | return '' + v; 197 | }, 198 | nullValue: function(v) { 199 | return '~'; 200 | } 201 | }; 202 | 203 | // Get the correct handler corresponding to variable type 204 | function processYAMLVariable(v, lvl, indent) { 205 | 206 | // Scalars 207 | if (typeof v === 'string') 208 | return yml.string(v); 209 | else if (typeof v === 'number') 210 | return yml.number(v); 211 | else if (typeof v === 'boolean') 212 | return yml.boolean(v); 213 | else if (typeof v === 'undefined' || v === null || isRealNaN(v)) 214 | return yml.nullValue(v); 215 | 216 | // Nonscalars 217 | else if (isPlainObject(v)) 218 | return yml.object(v, lvl, indent); 219 | else if (isArray(v)) 220 | return yml.array(v, lvl); 221 | else if (typeof v === 'function') 222 | return yml.fn(v); 223 | 224 | // Error 225 | else 226 | throw TypeError('artoo.writers.processYAMLVariable: wrong type.'); 227 | } 228 | 229 | // Converting JavaScript variables to a YAML string 230 | function toYAMLString(data) { 231 | return '---\n' + processYAMLVariable(data); 232 | } 233 | 234 | 235 | /** 236 | * Web Formats 237 | * ------------ 238 | * 239 | * Converts JavaScript data into standard web formats such as querystrings. 240 | */ 241 | 242 | function toQueryString(o, fn) { 243 | if (!isPlainObject(o)) 244 | throw Error('artoo.writers.queryString: wrong arguments.'); 245 | 246 | var s = '', 247 | k; 248 | 249 | for (k in o) { 250 | s += 251 | (s ? '&' : '') + 252 | k + '=' + 253 | encodeURIComponent(typeof fn === 'function' ? fn(o[k]) : o[k]); 254 | } 255 | 256 | return s; 257 | } 258 | 259 | function toCookie(key, value, params) { 260 | params = params || {}; 261 | 262 | var cookie = key + '=' + encodeURIComponent(value); 263 | 264 | if (params.days) { 265 | var date = new Date(); 266 | date.setTime(date.getTime() + (params.days * 24 * 60 * 60 * 1000)); 267 | cookie += '; expires=' + date.toGMTString(); 268 | } 269 | 270 | if (params.path) 271 | cookie += '; path=' + params.path; 272 | 273 | if (params.domain) 274 | cookie += '; domain=' + params.domain; 275 | 276 | if (params.httpOnly) 277 | cookie += '; HttpOnly'; 278 | 279 | if (params.secure) 280 | cookie += '; Secure'; 281 | 282 | return cookie; 283 | } 284 | 285 | 286 | /** 287 | * Exporting 288 | */ 289 | artoo.writers = { 290 | cookie: toCookie, 291 | csv: toCSVString, 292 | queryString: toQueryString, 293 | yaml: toYAMLString 294 | }; 295 | }).call(this); 296 | -------------------------------------------------------------------------------- /src/chrome/artoo.chrome.js: -------------------------------------------------------------------------------- 1 | ;(function(undefined) { 2 | 'use strict'; 3 | 4 | /** 5 | * artoo compilation of chrome relevant methods 6 | * ============================================= 7 | * 8 | * Useful to send data from the page context to an injected script and then 9 | * forward to the background script to store some persistent data. 10 | */ 11 | var _root = this; 12 | 13 | // Namespace 14 | artoo.chrome = {}; 15 | 16 | // Communication method 17 | }).call(this); 18 | -------------------------------------------------------------------------------- /src/common/artoo.methods.asyncStore.js: -------------------------------------------------------------------------------- 1 | ;(function(undefined) { 2 | 'use strict'; 3 | 4 | /** 5 | * artoo asynchronous store methods 6 | * ================================= 7 | * 8 | * artoo's abstraction of meta-browser storages such as chrome extensions 9 | * or phantomjs stores. 10 | */ 11 | var _root = this; 12 | 13 | /** 14 | * Abstract factory for asynchronous stores 15 | * ----------------------------------------- 16 | * 17 | * Return an helper function to access asynchronous stores such as the one 18 | * provided by the chrome extension or by phantomjs. 19 | * 20 | * An asynchronous store must be initiated with a sender function that would 21 | * enable communication between the page and the browser's utilities. 22 | * 23 | * Note that such a function must return a state-of-the-art promise. 24 | */ 25 | 26 | // Helpers 27 | function checkCallback() { 28 | return (typeof arguments[arguments.length - 1] !== 'function') ? 29 | artoo.$.Deferred() : 30 | arguments[arguments.length - 1]; 31 | } 32 | 33 | // TODO: enhance and ajust to promise polymorphism 34 | function brokenPipe() { 35 | throw Error('artoo.asyncStore: broken pipe.'); 36 | } 37 | 38 | // The factory itself 39 | function AsyncStoreFactory(sender) { 40 | 41 | if (typeof sender !== 'function') 42 | throw TypeError('artoo.asyncStore: expecting a sender function.'); 43 | 44 | // Communication 45 | function communicate(action, params, promise) { 46 | if (arguments.length < 3) { 47 | promise = params; 48 | params = null; 49 | } 50 | 51 | sender(action, params) 52 | .then(function() { 53 | if (typeof promise !== 'function') 54 | promise.resolve.apply(null, arguments); 55 | else 56 | promise.apply(null, arguments); 57 | }) 58 | .fail(brokenPipe); 59 | 60 | return typeof promise !== 'function' ? promise : undefined; 61 | } 62 | 63 | // Returning a function 64 | var store = function(key) { 65 | return store.get(key); 66 | }; 67 | 68 | // Methods 69 | store.get = function(key, cb) { 70 | var promise = checkCallback.apply(this, arguments); 71 | 72 | if (!key) 73 | return store.getAll(); 74 | 75 | // Requesting data 76 | return communicate('get', {key: key}, promise); 77 | }; 78 | 79 | store.getAll = function(cb) { 80 | var promise = checkCallback.apply(this, arguments); 81 | 82 | // Requesting data 83 | return communicate('getAll', promise); 84 | }; 85 | 86 | store.keys = function(cb) { 87 | var promise = checkCallback.apply(this, arguments); 88 | 89 | // Requesting store's keys 90 | return communicate('keys', promise); 91 | }; 92 | 93 | store.set = function(key, value, cb) { 94 | var promise = checkCallback.apply(this, arguments); 95 | 96 | if (typeof key !== 'string' && typeof key !== 'number') 97 | throw TypeError('artoo.store.set: trying to set an invalid key.'); 98 | 99 | return communicate('set', {key: key, value: value}, promise); 100 | }; 101 | 102 | store.remove = function(key, cb) { 103 | var promise = checkCallback.apply(this, arguments); 104 | 105 | if (typeof key !== 'string' && typeof key !== 'number') 106 | throw TypeError('artoo.store.set: trying to remove an invalid key.'); 107 | 108 | return communicate('remove', {key: key}, promise); 109 | }; 110 | 111 | store.removeAll = function(cb) { 112 | var promise = checkCallback.apply(this, arguments); 113 | 114 | return communicate('removeAll', promise); 115 | }; 116 | 117 | store.clear = store.removeAll; 118 | 119 | return store; 120 | } 121 | 122 | // Exporting factory 123 | artoo.createAsyncStore = AsyncStoreFactory; 124 | }).call(this); 125 | -------------------------------------------------------------------------------- /src/methods/artoo.methods.ajaxSniffer.js: -------------------------------------------------------------------------------- 1 | ;(function(undefined) { 2 | 'use strict'; 3 | 4 | /** 5 | * artoo ajax sniffer 6 | * =================== 7 | * 8 | * A useful ajax request sniffer. 9 | */ 10 | var _root = this, 11 | before = artoo.helpers.before; 12 | 13 | // Persistent state 14 | var originalXhr = { 15 | open: XMLHttpRequest.prototype.open, 16 | send: XMLHttpRequest.prototype.send, 17 | setRequestHeader: XMLHttpRequest.prototype.setRequestHeader 18 | }; 19 | 20 | // Main abstraction 21 | // TODO: working criteria 22 | // TODO: fire exception one step above 23 | function AjaxSniffer() { 24 | var self = this; 25 | 26 | // Properties 27 | this.hooked = false; 28 | this.listeners = []; 29 | 30 | // Privates 31 | function hook() { 32 | if (self.hooked) 33 | return; 34 | 35 | // Monkey patching the 'open' method 36 | XMLHttpRequest.prototype.open = before( 37 | XMLHttpRequest.prototype.open, 38 | function(method, url, async) { 39 | var xhr = this; 40 | 41 | // Overloading the xhr object 42 | xhr._spy = { 43 | method: method, 44 | url: url, 45 | params: artoo.parsers.url(url).query 46 | }; 47 | } 48 | ); 49 | 50 | // Monkey patching the 'send' method 51 | XMLHttpRequest.prototype.send = before( 52 | XMLHttpRequest.prototype.send, 53 | function(data) { 54 | var xhr = this; 55 | 56 | // Overloading the xhr object 57 | if (data) { 58 | xhr._spy.querystring = data; 59 | xhr._spy.data = artoo.parsers.queryString(data); 60 | } 61 | 62 | // Triggering listeners 63 | self.listeners.forEach(function(listener) { 64 | if (listener.criteria === '*') 65 | listener.fn.call(xhr, xhr._spy); 66 | }); 67 | } 68 | ); 69 | 70 | self.hooked = true; 71 | } 72 | 73 | function release() { 74 | if (!self.hooked) 75 | return; 76 | 77 | XMLHttpRequest.prototype.send = originalXhr.send; 78 | XMLHttpRequest.prototype.open = originalXhr.open; 79 | 80 | self.hooked = false; 81 | } 82 | 83 | // Methods 84 | this.before = function(criteria, callback) { 85 | 86 | // Polymorphism 87 | if (typeof criteria === 'function') { 88 | callback = criteria; 89 | criteria = null; 90 | } 91 | 92 | criteria = criteria || {}; 93 | 94 | // Hooking xhr 95 | hook(); 96 | 97 | // Binding listener 98 | this.listeners.push({criteria: '*', fn: callback}); 99 | }; 100 | 101 | this.after = function(criteria, callback) { 102 | 103 | // Polymorphism 104 | if (typeof criteria === 'function') { 105 | callback = criteria; 106 | criteria = null; 107 | } 108 | 109 | criteria = criteria || {}; 110 | 111 | // Hooking xhr 112 | hook(); 113 | 114 | // Binding a deviant listener 115 | this.listeners.push({criteria: '*', fn: function() { 116 | var xhr = this; 117 | 118 | xhr.addEventListener('load', function() { 119 | 120 | // Retrieving data as per response headers 121 | var contentType = xhr.getResponseHeader('Content-Type'), 122 | data = xhr.response; 123 | 124 | if (contentType && ~contentType.search(/json/)) { 125 | try { 126 | data = JSON.parse(xhr.responseText); 127 | } 128 | catch (e) { 129 | // pass... 130 | } 131 | } 132 | else if (contentType && ~contentType.search(/xml/)) { 133 | data = xhr.responseXML; 134 | } else { 135 | try { 136 | data = JSON.parse(xhr.responseText); 137 | } catch (e) { 138 | data = xhr.responseText; 139 | } 140 | } 141 | 142 | callback.call(xhr, xhr._spy, { 143 | data: data, 144 | headers: artoo.parsers.headers(xhr.getAllResponseHeaders()) 145 | }); 146 | }, false); 147 | }}); 148 | }; 149 | 150 | this.off = function(fn) { 151 | 152 | // Splicing function from listeners 153 | var index = artoo.helpers.indexOf(this.listeners, function(listener) { 154 | return listener.fn === fn; 155 | }); 156 | 157 | // Incorrect function 158 | if (!~index) 159 | throw Error('artoo.ajaxSniffer.off: trying to remove an inexistant ' + 160 | 'listener.'); 161 | 162 | this.listeners.splice(index, 1); 163 | 164 | // If no listeners were to remain, we release xhr 165 | if (!this.listeners.length) 166 | release(); 167 | }; 168 | } 169 | 170 | // Namespace 171 | artoo.ajaxSniffer = new AjaxSniffer(); 172 | }).call(this); 173 | -------------------------------------------------------------------------------- /src/methods/artoo.methods.ajaxSpider.js: -------------------------------------------------------------------------------- 1 | ;(function(undefined) { 2 | 'use strict'; 3 | 4 | /** 5 | * artoo ajaxSpider method 6 | * ======================== 7 | * 8 | * A useful method to scrape data from a list of ajax urls. 9 | */ 10 | var _root = this; 11 | 12 | function loop(list, params, i, acc, lastData) { 13 | acc = acc || []; 14 | i = i || 0; 15 | 16 | var o = (typeof list === 'function') ? list(i, lastData) : list[i]; 17 | 18 | // Breaking if iterator returns a falsy value 19 | if (!o) 20 | return params.done(acc); 21 | 22 | function get(c) { 23 | if (o.settings || params.settings) 24 | artoo.$.ajax( 25 | o.url || params.url || o, 26 | artoo.helpers.extend( 27 | o.settings || params.settings, 28 | { 29 | success: c, 30 | data: o.data || params.data || {}, 31 | type: o.method || params.method || 'get' 32 | } 33 | ) 34 | ); 35 | else 36 | artoo.$[o.method || params.method || 'get']( 37 | o.url || params.url || o, 38 | o.data || params.data || {}, 39 | c 40 | ); 41 | } 42 | 43 | // Getting data with ajax 44 | if (params.throttle > 0) 45 | setTimeout(get, !i ? 0 : params.throttle, dataRetrieved); 46 | else if (typeof params.throttle === 'function') 47 | setTimeout(get, !i ? 0 : params.throttle(i), dataRetrieved); 48 | else 49 | get(dataRetrieved); 50 | 51 | function dataRetrieved(data) { 52 | 53 | // Applying callback on data 54 | var result = data; 55 | 56 | if (params.scrape || params.scrapeOne || params.jquerify) 57 | data = artoo.helpers.jquerify(data); 58 | 59 | if (params.scrape || params.scrapeOne) { 60 | var chosenScraper = params.scrape ? 'scrape' : 'scrapeOne'; 61 | result = artoo[chosenScraper]( 62 | data.find(params[chosenScraper].iterator), 63 | params[chosenScraper].data, 64 | params[chosenScraper].params 65 | ); 66 | } 67 | else if (typeof params.process === 'function') { 68 | result = params.process(data, i, acc); 69 | } 70 | 71 | // If false is returned as the callback, we break 72 | if (result === false) 73 | return params.done(acc); 74 | 75 | // Concat or push? 76 | if (params.concat) 77 | acc = acc.concat(result); 78 | else 79 | acc.push(result); 80 | 81 | // Incrementing 82 | i++; 83 | 84 | if ((artoo.helpers.isArray(list) && i === list.length) || 85 | i === params.limit) 86 | params.done(acc); 87 | else 88 | loop(list, params, i, acc, data); 89 | } 90 | } 91 | 92 | // TODO: asynchronous 93 | artoo.ajaxSpider = function(list, params, cb) { 94 | var fn, 95 | p; 96 | 97 | // Default 98 | params = params || {}; 99 | 100 | // If only callback 101 | if (typeof params === 'function') { 102 | fn = params; 103 | params = {}; 104 | params.done = fn; 105 | } 106 | 107 | // Dealing with callback polymorphism 108 | if (typeof cb === 'function') 109 | p = artoo.helpers.extend({done: cb}, params); 110 | 111 | loop(list, artoo.helpers.extend(p || params, {done: artoo.helpers.noop})); 112 | }; 113 | }).call(this); 114 | -------------------------------------------------------------------------------- /src/methods/artoo.methods.autoExpand.js: -------------------------------------------------------------------------------- 1 | ;(function(undefined) { 2 | 'use strict'; 3 | 4 | /** 5 | * artoo autoExpand methods 6 | * ========================= 7 | * 8 | * Some useful functions to expand programmatically some content in 9 | * the scraped web page. 10 | */ 11 | var _root = this; 12 | 13 | function _expand(params, i, c) { 14 | i = i || 0; 15 | 16 | var canExpand = (params.canExpand) ? 17 | (typeof params.canExpand === 'string' ? 18 | artoo.$(params.canExpand).length > 0 : 19 | params.canExpand(artoo.$)) : 20 | true; 21 | 22 | // Is this over? 23 | if (!canExpand || i >= params.limit) { 24 | if (typeof params.done === 'function') 25 | params.done(); 26 | return; 27 | } 28 | 29 | // Triggering expand 30 | var expandFn = (typeof params.expand === 'string') ? 31 | function() { 32 | artoo.$(params.expand).simulate('click'); 33 | } : 34 | params.expand; 35 | 36 | if (params.throttle) 37 | setTimeout( 38 | expandFn, 39 | typeof params.throttle === 'function' ? 40 | params.throttle(i) : 41 | params.throttle, 42 | artoo.$ 43 | ); 44 | else 45 | expandFn(artoo.$); 46 | 47 | // Waiting expansion 48 | if (params.isExpanding) { 49 | 50 | // Checking whether the content is expanding and waiting for it to end. 51 | if (typeof params.isExpanding === 'number') { 52 | setTimeout(_expand, params.isExpanding, params, ++i); 53 | } 54 | else { 55 | var isExpanding = (typeof params.isExpanding === 'string') ? 56 | function() { 57 | return artoo.$(params.isExpanding).length > 0; 58 | } : 59 | params.isExpanding; 60 | 61 | artoo.waitFor( 62 | function() { 63 | return !isExpanding(artoo.$); 64 | }, 65 | function() { 66 | _expand(params, ++i); 67 | }, 68 | {timeout: params.timeout} 69 | ); 70 | } 71 | } 72 | else if (params.elements) { 73 | c = c || artoo.$(params.elements).length; 74 | 75 | // Counting elements to check if those have changed 76 | artoo.waitFor( 77 | function() { 78 | return artoo.$(params.elements).length > c; 79 | }, 80 | function() { 81 | _expand(params, ++i, artoo.$(params.elements).length); 82 | }, 83 | {timeout: params.timeout} 84 | ); 85 | } 86 | else { 87 | 88 | // No way to assert content changes, continuing... 89 | _expand(params, ++i); 90 | } 91 | } 92 | 93 | // TODO: throttle (make wrapper with setTimeout) 94 | artoo.autoExpand = function(params, cb) { 95 | params = params || {}; 96 | params.done = cb || params.done; 97 | 98 | if (!params.expand) 99 | throw Error('artoo.autoExpand: you must provide an expand parameter.'); 100 | 101 | _expand(params); 102 | }; 103 | }).call(this); 104 | -------------------------------------------------------------------------------- /src/methods/artoo.methods.autoScroll.js: -------------------------------------------------------------------------------- 1 | ;(function(undefined) { 2 | 'use strict'; 3 | 4 | /** 5 | * artoo autoScroll methods 6 | * ========================= 7 | * 8 | * Some useful functions to scroll programmatically the web pages you need 9 | * to scrape. 10 | */ 11 | var _root = this; 12 | 13 | artoo.autoScroll = function(params, cb) { 14 | artoo.autoExpand( 15 | this.helpers.extend(params, { 16 | expand: function() { 17 | window.scrollTo(0, document.body.scrollHeight); 18 | } 19 | }), 20 | cb 21 | ); 22 | }; 23 | }).call(this); 24 | -------------------------------------------------------------------------------- /src/methods/artoo.methods.cookies.js: -------------------------------------------------------------------------------- 1 | ;(function(undefined) { 2 | 'use strict'; 3 | 4 | /** 5 | * artoo cookies methods 6 | * ====================== 7 | * 8 | * artoo's abstraction to handle the page's cookies. 9 | */ 10 | var _root = this; 11 | 12 | artoo.cookies = function(key) { 13 | return artoo.cookies.get(key); 14 | }; 15 | 16 | artoo.cookies.get = function(key) { 17 | var cookies = artoo.parsers.cookies(document.cookie); 18 | return (key ? cookies[key] : cookies); 19 | }; 20 | 21 | artoo.cookies.getAll = function() { 22 | return artoo.cookies.get(); 23 | }; 24 | 25 | artoo.cookies.set = function(key, value, params) { 26 | document.cookie = artoo.writers.cookie(key, value, params); 27 | }; 28 | 29 | artoo.cookies.remove = function(key, params) { 30 | var p = artoo.helpers.extend(params); 31 | 32 | // Ensuring no days were passed 33 | delete p.days; 34 | 35 | var cookie = artoo.writers.cookie(key, '*', p); 36 | 37 | // Passed expiration 38 | cookie += ' ;expires=Thu, 01 Jan 1970 00:00:01 GMT'; 39 | 40 | document.cookie = cookie; 41 | }; 42 | 43 | artoo.cookies.removeAll = function() { 44 | var cookies = artoo.cookies.getAll(), 45 | k; 46 | 47 | for (k in cookies) 48 | artoo.cookies.remove(k); 49 | }; 50 | 51 | artoo.cookies.clear = artoo.cookies.removeAll; 52 | }).call(this); 53 | -------------------------------------------------------------------------------- /src/methods/artoo.methods.save.js: -------------------------------------------------------------------------------- 1 | ;(function(undefined) { 2 | 'use strict'; 3 | 4 | /** 5 | * artoo save methods 6 | * =================== 7 | * 8 | * Some helpers to save data to a file that will be downloaded by the 9 | * browser. Works mainly with chrome for the time being. 10 | * 11 | */ 12 | var _root = this, 13 | helpers = artoo.helpers; 14 | 15 | // Polyfills 16 | var URL = _root.URL || _root.webkitURL || _root; 17 | 18 | // Utilities 19 | function selectorOuterHTML($sel) { 20 | return ($sel[0].documentElement && $sel[0].documentElement.outerHTML) || 21 | $sel[0].outerHTML; 22 | } 23 | 24 | function filenamePolymorphism(params) { 25 | return (typeof params === 'string') ? {filename: params} : params || {}; 26 | } 27 | 28 | // Main abstraction 29 | function Saver() { 30 | var _saver; 31 | 32 | // Properties 33 | this.defaultFilename = 'artoo_data'; 34 | this.defaultEncoding = 'utf-8'; 35 | this.xmlns = 'http://www.w3.org/1999/xhtml'; 36 | this.mimeShortcuts = { 37 | csv: 'text/csv', 38 | tsv: 'text/tab-separated-values', 39 | json: 'application/json', 40 | txt: 'text/plain', 41 | html: 'text/html', 42 | yaml: 'text/yaml' 43 | }; 44 | 45 | // Methods 46 | this.createBlob = function(data, mime, encoding) { 47 | mime = this.mimeShortcuts[mime] || mime || this.defaultMime; 48 | return new Blob( 49 | [data], 50 | {type: mime + ';charset=' + encoding || this.defaultEncoding} 51 | ); 52 | }; 53 | 54 | this.createBlobFromDataURL = function(url) { 55 | var byteString = atob(url.split(',')[1]), 56 | ba = new Uint8Array(byteString.length), 57 | i, 58 | l; 59 | 60 | for (i = 0, l = byteString.length; i < l; i++) 61 | ba[i] = byteString.charCodeAt(i); 62 | 63 | return new Blob([ba.buffer], { 64 | type: url.split(',')[0].split(':')[1].split(';')[0] 65 | }); 66 | }; 67 | 68 | this.blobURL = function(blob) { 69 | var oURL = URL.createObjectURL(blob); 70 | return oURL; 71 | }; 72 | 73 | this.saveResource = function(href, params) { 74 | var a = document.createElementNS(this.xmlns, 'a'); 75 | a.href = href; 76 | 77 | a.setAttribute('download', params.filename || ''); 78 | 79 | // Firefox needs the link attached to the page's DOM 80 | if ('document' in _root) 81 | document.body.appendChild(a); 82 | 83 | a.click(); 84 | 85 | if ('document' in _root) 86 | document.body.removeChild(a); 87 | a = null; 88 | 89 | // Revoking the object URL if we want to 90 | if (params.revoke) 91 | setTimeout(function() { 92 | URL.revokeObjectURL(href); 93 | }); 94 | }; 95 | 96 | // Main interface 97 | this.saveData = function(data, params) { 98 | params = params || {}; 99 | 100 | // Creating the blob 101 | var blob = this.createBlob(data, params.mime, params.encoding); 102 | 103 | // Saving the blob 104 | this.saveResource( 105 | this.blobURL(blob), 106 | { 107 | filename: params.filename || this.defaultFilename, 108 | revoke: params.revoke || true 109 | } 110 | ); 111 | }; 112 | 113 | this.saveDataURL = function(url, params) { 114 | params = params || {}; 115 | 116 | // Creating the blob 117 | var blob = this.createBlobFromDataURL(url); 118 | 119 | // Saving the blob 120 | this.saveResource( 121 | blob, 122 | {filename: params.filename || this.defaultFilename} 123 | ); 124 | }; 125 | } 126 | 127 | var _saver = new Saver(); 128 | 129 | // Exporting 130 | artoo.save = function(data, params) { 131 | _saver.saveData(data, filenamePolymorphism(params)); 132 | }; 133 | 134 | artoo.saveJson = function(data, params) { 135 | params = filenamePolymorphism(params); 136 | 137 | // Enforcing json 138 | if (typeof data !== 'string') { 139 | if (params.pretty || params.indent) 140 | data = JSON.stringify(data, undefined, params.indent || 2); 141 | else 142 | data = JSON.stringify(data); 143 | } 144 | else { 145 | if (params.pretty || params.indent) 146 | data = JSON.stringify(JSON.parse(data), undefined, params.indent || 2); 147 | } 148 | 149 | // Extending params 150 | artoo.save( 151 | data, 152 | helpers.extend(params, {filename: 'data.json', mime: 'json'}) 153 | ); 154 | }; 155 | 156 | artoo.savePrettyJson = function(data, params) { 157 | params = filenamePolymorphism(params); 158 | artoo.saveJson(data, helpers.extend(params, {pretty: true})); 159 | }; 160 | 161 | artoo.saveYaml = function(data, params) { 162 | params = filenamePolymorphism(params); 163 | artoo.save( 164 | artoo.writers.yaml(data), 165 | helpers.extend(params, {filename: 'data.yml', mime: 'yaml'}) 166 | ); 167 | }; 168 | 169 | artoo.saveCsv = function(data, params) { 170 | params = filenamePolymorphism(params); 171 | 172 | data = (typeof data !== 'string') ? 173 | artoo.writers.csv(data, params) : 174 | data; 175 | 176 | artoo.save( 177 | data, 178 | helpers.extend(params, {mime: 'csv', filename: 'data.csv'}) 179 | ); 180 | }; 181 | 182 | artoo.saveTsv = function(data, params) { 183 | artoo.saveCsv( 184 | data, 185 | helpers.extend(filenamePolymorphism(params), { 186 | mime: 'tsv', 187 | delimiter: '\t', 188 | filename: 'data.tsv' 189 | }) 190 | ); 191 | }; 192 | 193 | artoo.saveXml = function(data, params) { 194 | params = filenamePolymorphism(params); 195 | 196 | var s = (helpers.isSelector(data) && selectorOuterHTML(data)) || 197 | (helpers.isDocument(data) && data.documentElement.outerHTML) || 198 | data, 199 | type = params.type || 'xml', 200 | header = ''; 201 | 202 | // Determining doctype 203 | if (type === 'html' && helpers.isDocument(data)) { 204 | var dt = data.doctype; 205 | 206 | if (dt) 207 | header = '\n'; 210 | } 211 | else if (type === 'xml' || type === 'svg') { 212 | if (!~s.search(/<\?xml/)) 213 | header = '\n'; 216 | } 217 | 218 | if (type === 'svg') { 219 | header += '\n'; 221 | } 222 | 223 | artoo.save( 224 | header + s, 225 | helpers.extend( 226 | params, 227 | {mime: 'html', filename: 'document.xml'}) 228 | ); 229 | }; 230 | 231 | artoo.saveHtml = function(data, params) { 232 | artoo.saveXml( 233 | data, 234 | helpers.extend( 235 | filenamePolymorphism(params), 236 | {filename: 'document.html', type: 'html'} 237 | ) 238 | ); 239 | }; 240 | 241 | artoo.savePageHtml = function(params) { 242 | artoo.saveHtml( 243 | document, 244 | helpers.extend(filenamePolymorphism(params), {filename: 'page.html'}) 245 | ); 246 | }; 247 | 248 | artoo.saveSvg = function(sel, params) { 249 | params = filenamePolymorphism(params); 250 | 251 | var $sel = artoo.$(sel); 252 | if (!$sel.is('svg')) 253 | throw Error('artoo.saveSvg: selector is not svg.'); 254 | 255 | artoo.saveXml( 256 | $sel, 257 | helpers.extend(params, {filename: 'drawing.svg', type: 'svg'}) 258 | ); 259 | }; 260 | 261 | artoo.saveStore = function(params) { 262 | params = filenamePolymorphism(params); 263 | artoo.savePrettyJson( 264 | artoo.store.get(params.key), 265 | helpers.extend(params, {filename: 'store.json'}) 266 | ); 267 | }; 268 | 269 | artoo.saveResource = function(url, params) { 270 | _saver.saveResource(url, filenamePolymorphism(params)); 271 | }; 272 | 273 | artoo.saveImage = function(sel, params) { 274 | params = filenamePolymorphism(params); 275 | 276 | var $sel = artoo.$(sel); 277 | 278 | if (!$sel.is('img') && !$sel.attr('src')) 279 | throw Error('artoo.saveImage: selector is not an image.'); 280 | 281 | var ext = helpers.getExtension($sel.attr('src')), 282 | alt = $sel.attr('alt'); 283 | 284 | artoo.saveResource( 285 | $sel.attr('src'), 286 | helpers.extend( 287 | params, 288 | { 289 | filename: alt ? alt + (ext ? '.' + ext : '') : false 290 | } 291 | ) 292 | ); 293 | }; 294 | }).call(this); 295 | -------------------------------------------------------------------------------- /src/methods/artoo.methods.scrape.js: -------------------------------------------------------------------------------- 1 | ;(function(undefined) { 2 | 'use strict'; 3 | 4 | /** 5 | * artoo scrape methods 6 | * ===================== 7 | * 8 | * Some scraping helpers. 9 | */ 10 | var _root = this, 11 | extend = artoo.helpers.extend; 12 | 13 | /** 14 | * Helpers 15 | */ 16 | function step(o, scope) { 17 | var $ = artoo.$, 18 | $sel = o.sel ? $(scope).find(o.sel) : $(scope), 19 | val; 20 | 21 | // Polymorphism 22 | if (typeof o === 'function') { 23 | val = o.call(scope, $, $sel.get()); 24 | } 25 | else if (typeof o.method === 'function') 26 | val = o.method.call($sel.get(), $, $sel.get()); 27 | else if (typeof o === 'string') { 28 | if (typeof $sel[o] === 'function') 29 | val = $sel[o](); 30 | else 31 | val = $sel.attr(o); 32 | } 33 | else { 34 | val = (o.attr !== undefined) ? 35 | $sel.attr(o.attr) : 36 | $sel[o.method || 'text'](); 37 | } 38 | 39 | // Default value? 40 | if (o.defaultValue && !val) 41 | val = o.defaultValue; 42 | 43 | return val; 44 | } 45 | 46 | // Scraping function after polymorphism has been taken care of 47 | function scrape(iterator, data, params, cb) { 48 | var $ = artoo.$, 49 | scraped = [], 50 | loneSelector = !!data.attr || !!data.method || data.scrape || 51 | typeof data === 'string' || 52 | typeof data === 'function'; 53 | 54 | params = params || {}; 55 | 56 | // Transforming to selector 57 | var $iterator; 58 | if (typeof iterator === 'function') 59 | $iterator = $(iterator($)); 60 | else 61 | $iterator = $(iterator); 62 | 63 | // Iteration 64 | $iterator.each(function(i) { 65 | var item = {}, 66 | p; 67 | 68 | // TODO: figure iteration scope elsewhere for scrape recursivity 69 | if (loneSelector) 70 | item = (typeof data === 'object' && 'scrape' in data) ? 71 | scrape( 72 | (data.sel ? $(this).find(data.sel) : $(this)) 73 | .find(data.scrape.iterator), 74 | data.scrape.data, 75 | data.scrape.params 76 | ) : 77 | step(data, this); 78 | else 79 | for (p in data) { 80 | item[p] = (typeof data[p] === 'object' && 'scrape' in data[p]) ? 81 | scrape( 82 | (data[p].sel ? $(this).find(data[p].sel) : $(this)) 83 | .find(data[p].scrape.iterator), 84 | data[p].scrape.data, 85 | data[p].scrape.params 86 | ) : 87 | step(data[p], this); 88 | } 89 | 90 | scraped.push(item); 91 | 92 | // Breaking if limit i attained 93 | return !params.limit || i < params.limit - 1; 94 | }); 95 | 96 | scraped = params.one ? scraped[0] : scraped; 97 | 98 | // Triggering callback 99 | if (typeof cb === 'function') 100 | cb(scraped); 101 | 102 | // Returning data 103 | return scraped; 104 | } 105 | 106 | // Function taking care of harsh polymorphism 107 | function polymorphism(iterator, data, params, cb) { 108 | var h = artoo.helpers, 109 | i, d, p, c; 110 | 111 | if (h.isPlainObject(iterator) && 112 | !h.isSelector(iterator) && 113 | !h.isDocument(iterator) && 114 | (iterator.iterator || iterator.data || iterator.params)) { 115 | d = iterator.data; 116 | p = h.isPlainObject(iterator.params) ? iterator.params : {}; 117 | i = iterator.iterator; 118 | } 119 | else { 120 | d = data; 121 | p = h.isPlainObject(params) ? params : {}; 122 | i = iterator; 123 | } 124 | 125 | // Default values 126 | d = d || 'text'; 127 | 128 | c = typeof cb === 'function' ? cb : 129 | typeof params === 'function' ? params : 130 | p.done; 131 | 132 | return [i, d, p, c]; 133 | } 134 | 135 | /** 136 | * Public interface 137 | */ 138 | artoo.scrape = function(iterator, data, params, cb) { 139 | var args = polymorphism(iterator, data, params, cb); 140 | 141 | // Warn if no iterator or no data 142 | if (!args[0] || !args[1]) 143 | throw TypeError('artoo.scrape: wrong arguments.'); 144 | 145 | return scrape.apply(this, args); 146 | }; 147 | 148 | // Scrape only the first corresponding item 149 | artoo.scrapeOne = function(iterator, data, params, cb) { 150 | var args = polymorphism(iterator, data, params, cb); 151 | 152 | // Extending parameters 153 | args[2] = artoo.helpers.extend(args[2], {limit: 1, one: true}); 154 | 155 | return scrape.apply(this, args); 156 | }; 157 | 158 | // Scrape a table 159 | // TODO: handle different contexts 160 | // TODO: better header handle 161 | artoo.scrapeTable = function(root, params, cb) { 162 | var $ = artoo.$; 163 | 164 | params = params || {}; 165 | 166 | var sel = root, 167 | headers; 168 | 169 | if (!params.headers) { 170 | return artoo.scrape($(sel).find('tr:has(td)'), { 171 | scrape: { 172 | iterator: 'td', 173 | data: params.data || 'text' 174 | } 175 | }, params, cb); 176 | } 177 | else { 178 | var headerType = params.headers.type || 179 | params.headers.method && 'first' || 180 | params.headers, 181 | headerFn = params.headers.method; 182 | 183 | if (headerType === 'th') { 184 | headers = artoo.scrape( 185 | $(sel).find('th'), headerFn || 'text' 186 | ); 187 | } 188 | else if (headerType === 'first') { 189 | headers = artoo.scrape( 190 | $(sel).find(' tr:has(td):first-of-type td'), 191 | headerFn || 'text' 192 | ); 193 | } 194 | else if (artoo.helpers.isArray(headerType)) { 195 | headers = headerType; 196 | } 197 | else { 198 | throw TypeError('artoo.scrapeTable: wrong headers type.'); 199 | } 200 | 201 | // Scraping 202 | return artoo.scrape( 203 | $(sel).find('tr:has(td)' + 204 | (headerType === 'first' ? ':not(:first-of-type)' : '')), function() { 205 | var o = {}; 206 | 207 | headers.forEach(function(h, i) { 208 | o[h] = step( 209 | params.data || 'text', 210 | $(this).find('td:nth-of-type(' + (i + 1) + ')') 211 | ); 212 | }, this); 213 | 214 | return o; 215 | }, params, cb); 216 | } 217 | }; 218 | 219 | /** 220 | * jQuery plugin 221 | */ 222 | function _scrape($) { 223 | var methods = ['scrape', 'scrapeOne', 'scrapeTable']; 224 | 225 | methods.forEach(function(method) { 226 | 227 | $.fn[method] = function() { 228 | return artoo[method].apply( 229 | artoo, [$(this)].concat(Array.prototype.slice.call(arguments))); 230 | }; 231 | }); 232 | } 233 | 234 | // Exporting 235 | artoo.jquery.plugins.push(_scrape); 236 | 237 | }).call(this); 238 | -------------------------------------------------------------------------------- /src/methods/artoo.methods.store.js: -------------------------------------------------------------------------------- 1 | ;(function(undefined) { 2 | 'use strict'; 3 | 4 | /** 5 | * artoo store methods 6 | * ==================== 7 | * 8 | * artoo's abstraction of browser storages. 9 | */ 10 | var _root = this; 11 | 12 | // Utilities 13 | function isCache(key) { 14 | var d = artoo.settings.cache.delimiter; 15 | return key.charAt(0) === d && key.charAt(key.length - 1) === d; 16 | } 17 | 18 | /** 19 | * Abstract factory for synchronous stores 20 | * ---------------------------------------- 21 | * 22 | * Return an helper function to access simple HTML5 storages such as 23 | * localStorage and sessionStorage. 24 | * 25 | * Unfortunately, those storages are limited by the same-origin policy. 26 | */ 27 | function StoreFactory(engine) { 28 | 29 | // Initialization 30 | if (engine === 'local') 31 | engine = localStorage; 32 | else if (engine === 'session') 33 | engine = sessionStorage; 34 | else 35 | throw Error('artoo.store: wrong engine "' + engine + '".'); 36 | 37 | // Returning a function 38 | var store = function(key) { 39 | return store.get(key); 40 | }; 41 | 42 | // Methods 43 | store.get = function(key) { 44 | if (!key) 45 | return store.getAll(); 46 | 47 | var v = engine.getItem(key); 48 | try { 49 | return JSON.parse(v); 50 | } 51 | catch (e) { 52 | return v; 53 | } 54 | }; 55 | 56 | store.getAll = function() { 57 | var s = {}; 58 | for (var i in engine) { 59 | if (!isCache(i)) 60 | s[i] = store.get(i); 61 | } 62 | return s; 63 | }; 64 | 65 | store.keys = function(key) { 66 | var keys = [], 67 | i; 68 | for (i in engine) 69 | keys.push(i); 70 | 71 | return keys; 72 | }; 73 | 74 | store.set = function(key, value) { 75 | if (typeof key !== 'string' && typeof key !== 'number') 76 | throw TypeError('artoo.store.set: trying to set an invalid key.'); 77 | 78 | // Storing 79 | engine.setItem(key, JSON.stringify(value)); 80 | }; 81 | 82 | store.pushTo = function(key, value) { 83 | var a = store.get(key); 84 | 85 | if (!artoo.helpers.isArray(a) && a !== null) 86 | throw TypeError('artoo.store.pushTo: trying to push to a non-array.'); 87 | 88 | a = a || []; 89 | a.push(value); 90 | store.set(key, a); 91 | return a; 92 | }; 93 | 94 | store.update = function(key, object) { 95 | var o = store.get(key); 96 | 97 | if (!artoo.helpers.isPlainObject(o) && o !== null) 98 | throw TypeError('artoo.store.update: trying to udpate to a non-object.'); 99 | 100 | o = artoo.helpers.extend(object, o); 101 | store.set(key, o); 102 | return o; 103 | }; 104 | 105 | store.remove = function(key) { 106 | 107 | if (typeof key !== 'string' && typeof key !== 'number') 108 | throw TypeError('artoo.store.set: trying to remove an invalid key.'); 109 | 110 | engine.removeItem(key); 111 | }; 112 | 113 | store.removeAll = function() { 114 | for (var i in engine) { 115 | if (!isCache(i)) 116 | engine.removeItem(i); 117 | } 118 | }; 119 | 120 | store.clear = store.removeAll; 121 | 122 | return store; 123 | } 124 | 125 | // Exporting factory 126 | artoo.createStore = StoreFactory; 127 | 128 | // Creating artoo's default store to be used 129 | artoo.store = StoreFactory(artoo.settings.store.engine); 130 | 131 | // Shortcuts 132 | artoo.s = artoo.store; 133 | }).call(this); 134 | -------------------------------------------------------------------------------- /src/methods/artoo.methods.ui.js: -------------------------------------------------------------------------------- 1 | ;(function(undefined) { 2 | 'use strict'; 3 | 4 | /** 5 | * artoo ui 6 | * ========= 7 | * 8 | * A handy utility to create shadow DOM interfaces on the fly. 9 | */ 10 | var _root = this; 11 | 12 | // Persistent state 13 | var COUNTER = 0, 14 | INSTANCES = {}; 15 | 16 | // Main Class 17 | artoo.ui = function(params) { 18 | params = params || {}; 19 | 20 | var id = params.id || 'artoo-ui' + (COUNTER++); 21 | 22 | // Referencing the instance 23 | this.name = params.name || id; 24 | INSTANCES[this.name] = this; 25 | 26 | // Creating a host 27 | this.mountNode = params.mountNode || artoo.mountNode; 28 | this.host = document.createElement('div'); 29 | this.host.setAttribute('id', id); 30 | this.mountNode.appendChild(this.host); 31 | 32 | // Properties 33 | this.shadow = this.host.attachShadow({mode: 'open'}); 34 | 35 | // Methods 36 | function init() { 37 | var stylesheets = params.stylesheets || params.stylesheet; 38 | if (stylesheets) { 39 | (artoo.helpers.isArray(stylesheets) ? 40 | stylesheets : [stylesheets]).forEach(function(s) { 41 | this.injectStyle(s); 42 | }, this); 43 | } 44 | } 45 | 46 | this.$ = function(sel) { 47 | return !sel ? 48 | artoo.$(this.shadow) : 49 | artoo.$(this.shadow).children(sel).add( 50 | artoo.$(this.shadow).children().find(sel) 51 | ); 52 | }; 53 | 54 | this.injectStyle = function(name) { 55 | if (!(name in artoo.stylesheets)) 56 | throw Error('artoo.ui.injectStyle: attempting to inject unknown ' + 57 | 'stylesheet (' + name +')'); 58 | 59 | this.injectInlineStyle(artoo.stylesheets[name]); 60 | }; 61 | 62 | this.injectInlineStyle = function(style) { 63 | 64 | // Creating a style tag 65 | var e = document.createElement('style'); 66 | e.innerHTML = (artoo.helpers.isArray(style)) ? 67 | style.join('\n') : 68 | style; 69 | 70 | // Appending to shadow 71 | this.shadow.appendChild(e); 72 | 73 | // Returning instance for chaining 74 | return this; 75 | }; 76 | 77 | this.kill = function() { 78 | this.mountNode.removeChild(this.host); 79 | delete this.shadow; 80 | delete this.host; 81 | delete INSTANCES[this.name]; 82 | }; 83 | 84 | // Initializing 85 | init.call(this); 86 | }; 87 | 88 | // Instances accessor 89 | artoo.ui.instances = function(key) { 90 | return key ? INSTANCES[key] : INSTANCES; 91 | }; 92 | }).call(this); 93 | -------------------------------------------------------------------------------- /src/node/artoo.node.helpers.js: -------------------------------------------------------------------------------- 1 | ;(function(undefined) { 2 | 'use strict'; 3 | 4 | /** 5 | * artoo helpers 6 | * ============== 7 | * 8 | * Replacing some helpers by their node.js counterparts. 9 | */ 10 | var _root = this; 11 | 12 | // False function 13 | artoo.helpers.isDocument = function(v) { 14 | return false; 15 | }; 16 | 17 | // Is this a cheerio selector? 18 | artoo.helpers.isSelector = function(v) { 19 | return !!(v && v.prototype && v.prototype.cheerio && 20 | v.prototype.cheerio === '[cheerio object]') || 21 | !!(v._root && v.options && 'normalizeWhitespace' in v.options); 22 | }; 23 | }).call(this); 24 | -------------------------------------------------------------------------------- /src/node/artoo.node.js: -------------------------------------------------------------------------------- 1 | ;(function(undefined) { 2 | 'use strict'; 3 | 4 | /** 5 | * artoo Node.js utilities 6 | * ======================== 7 | * 8 | * Some useful utilities when using artoo.js within node. 9 | */ 10 | var cheerio = require('cheerio'), 11 | path = require('path'); 12 | 13 | // Setting initial context 14 | artoo.$ = cheerio.load(''); 15 | 16 | // Methods 17 | artoo.bootstrap = function(cheerioInstance) { 18 | ['scrape', 'scrapeOne', 'scrapeTable'].forEach(function(m) { 19 | cheerioInstance.prototype[m] = function() { 20 | return artoo[m].apply( 21 | artoo, [artoo.$(this)].concat(Array.prototype.slice.call(arguments))); 22 | }; 23 | }); 24 | }; 25 | 26 | artoo.bootstrap(cheerio); 27 | 28 | artoo.setContext = function($) { 29 | 30 | // Fixing context 31 | artoo.$ = $; 32 | }; 33 | 34 | // Giving paths to alternative lib versions so they can be used afterwards 35 | artoo.paths = { 36 | browser: path.join(__dirname, 'artoo.concat.js'), 37 | chrome: path.join(__dirname, 'artoo.chrome.js'), 38 | phantom: path.join(__dirname, 'artoo.phantom.js') 39 | }; 40 | }).call(this); 41 | -------------------------------------------------------------------------------- /src/node/artoo.node.require.js: -------------------------------------------------------------------------------- 1 | /** 2 | * artoo node.js require 3 | * ====================== 4 | * 5 | * Simply exporting artoo through a node module. 6 | */ 7 | module.exports = artoo; 8 | -------------------------------------------------------------------------------- /src/node/artoo.node.shim.js: -------------------------------------------------------------------------------- 1 | /** 2 | * artoo node.js shim 3 | * =================== 4 | * 5 | * Make it possible to require artoo through node. 6 | */ 7 | var artoo = this.artoo; 8 | -------------------------------------------------------------------------------- /src/phantom/artoo.phantom.js: -------------------------------------------------------------------------------- 1 | ;(function(undefined) { 2 | 'use strict'; 3 | 4 | /** 5 | * artoo phantom bridging 6 | * ======================= 7 | * 8 | * Useful functions to send and receive data when spawned into a phantom.js 9 | * instance. 10 | */ 11 | var _root = this, 12 | passphrase = 'detoo'; 13 | 14 | // Safeguard 15 | if (!artoo.browser.phantomjs) 16 | throw Error('artoo.phantom: not in a phantom.js instance.'); 17 | 18 | // Namespacing 19 | artoo.phantom = {}; 20 | 21 | // Sending data to phantom 22 | artoo.phantom.send = function(head, body) { 23 | _root.callPhantom({head: head, body: body, passphrase: passphrase}); 24 | }; 25 | 26 | // Phantom notifying something to us 27 | artoo.phantom.notify = function(head, body) { 28 | artoo.emit('phantom:' + head, body); 29 | }; 30 | 31 | // Killing phantom from the page for testing purposes 32 | artoo.phantom.exit = function(code) { 33 | artoo.phantom.send('exit', code); 34 | }; 35 | 36 | // Streaming data to phantom 37 | artoo.phantom.stream = function(data) { 38 | artoo.phantom.send('stream', data); 39 | }; 40 | 41 | // Telling phantom the scraping is over 42 | artoo.phantom.done = function(err, data) { 43 | artoo.phantom.send('done', {error: err, data: data}); 44 | }; 45 | 46 | // Alias 47 | artoo.done = artoo.phantom.done; 48 | }).call(this); 49 | -------------------------------------------------------------------------------- /src/third_party/emmett.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | /** 5 | * Here is the list of every allowed parameter when using Emitter#on: 6 | * @type {Object} 7 | */ 8 | var __allowedOptions = { 9 | once: 'boolean', 10 | scope: 'object' 11 | }; 12 | 13 | 14 | /** 15 | * The emitter's constructor. It initializes the handlers-per-events store and 16 | * the global handlers store. 17 | * 18 | * Emitters are useful for non-DOM events communication. Read its methods 19 | * documentation for more information about how it works. 20 | * 21 | * @return {Emitter} The fresh new instance. 22 | */ 23 | var Emitter = function() { 24 | this._enabled = true; 25 | this._children = []; 26 | this._handlers = {}; 27 | this._handlersAll = []; 28 | }; 29 | 30 | 31 | /** 32 | * This method binds one or more functions to the emitter, handled to one or a 33 | * suite of events. So, these functions will be executed anytime one related 34 | * event is emitted. 35 | * 36 | * It is also possible to bind a function to any emitted event by not 37 | * specifying any event to bind the function to. 38 | * 39 | * Recognized options: 40 | * ******************* 41 | * - {?boolean} once If true, the handlers will be unbound after the first 42 | * execution. Default value: false. 43 | * - {?object} scope If a scope is given, then the listeners will be called 44 | * with this scope as "this". 45 | * 46 | * Variant 1: 47 | * ********** 48 | * > myEmitter.on('myEvent', function(e) { console.log(e); }); 49 | * > // Or: 50 | * > myEmitter.on('myEvent', function(e) { console.log(e); }, { once: true }); 51 | * 52 | * @param {string} event The event to listen to. 53 | * @param {function} handler The function to bind. 54 | * @param {?object} options Eventually some options. 55 | * @return {Emitter} Returns this. 56 | * 57 | * Variant 2: 58 | * ********** 59 | * > myEmitter.on( 60 | * > ['myEvent1', 'myEvent2'], 61 | * > function(e) { console.log(e); } 62 | * >); 63 | * > // Or: 64 | * > myEmitter.on( 65 | * > ['myEvent1', 'myEvent2'], 66 | * > function(e) { console.log(e); } 67 | * > { once: true }} 68 | * >); 69 | * 70 | * @param {array} events The events to listen to. 71 | * @param {function} handler The function to bind. 72 | * @param {?object} options Eventually some options. 73 | * @return {Emitter} Returns this. 74 | * 75 | * Variant 3: 76 | * ********** 77 | * > myEmitter.on({ 78 | * > myEvent1: function(e) { console.log(e); }, 79 | * > myEvent2: function(e) { console.log(e); } 80 | * > }); 81 | * > // Or: 82 | * > myEmitter.on({ 83 | * > myEvent1: function(e) { console.log(e); }, 84 | * > myEvent2: function(e) { console.log(e); } 85 | * > }, { once: true }); 86 | * 87 | * @param {object} bindings An object containing pairs event / function. 88 | * @param {?object} options Eventually some options. 89 | * @return {Emitter} Returns this. 90 | * 91 | * Variant 4: 92 | * ********** 93 | * > myEmitter.on(function(e) { console.log(e); }); 94 | * > // Or: 95 | * > myEmitter.on(function(e) { console.log(e); }, { once: true}); 96 | * 97 | * @param {function} handler The function to bind to every events. 98 | * @param {?object} options Eventually some options. 99 | * @return {Emitter} Returns this. 100 | */ 101 | Emitter.prototype.on = function(a, b, c) { 102 | var i, 103 | l, 104 | k, 105 | event, 106 | eArray, 107 | bindingObject; 108 | 109 | // Variant 1 and 2: 110 | if (typeof b === 'function') { 111 | eArray = typeof a === 'string' ? 112 | [a] : 113 | a; 114 | 115 | for (i = 0, l = eArray.length; i !== l; i += 1) { 116 | event = eArray[i]; 117 | 118 | // Check that event is not '': 119 | if (!event) 120 | continue; 121 | 122 | if (!this._handlers[event]) 123 | this._handlers[event] = []; 124 | 125 | bindingObject = { 126 | handler: b 127 | }; 128 | 129 | for (k in c || {}) 130 | if (__allowedOptions[k]) 131 | bindingObject[k] = c[k]; 132 | else 133 | throw new Error( 134 | 'The option "' + k + '" is not recognized by Emmett.' 135 | ); 136 | 137 | this._handlers[event].push(bindingObject); 138 | } 139 | 140 | // Variant 3: 141 | } else if (a && typeof a === 'object' && !Array.isArray(a)) 142 | for (event in a) 143 | Emitter.prototype.on.call(this, event, a[event], b); 144 | 145 | // Variant 4: 146 | else if (typeof a === 'function') { 147 | bindingObject = { 148 | handler: a 149 | }; 150 | 151 | for (k in c || {}) 152 | if (__allowedOptions[k]) 153 | bindingObject[k] = c[k]; 154 | else 155 | throw new Error( 156 | 'The option "' + k + '" is not recognized by Emmett.' 157 | ); 158 | 159 | this._handlersAll.push(bindingObject); 160 | } 161 | 162 | // No matching variant: 163 | else 164 | throw new Error('Wrong arguments.'); 165 | 166 | return this; 167 | }; 168 | 169 | 170 | /** 171 | * This method works exactly as the previous #on, but will add an options 172 | * object if none is given, and set the option "once" to true. 173 | * 174 | * The polymorphism works exactly as with the #on method. 175 | */ 176 | Emitter.prototype.once = function(a, b, c) { 177 | // Variant 1 and 2: 178 | if (typeof b === 'function') { 179 | c = c || {}; 180 | c.once = true; 181 | this.on(a, b, c); 182 | 183 | // Variants 3 and 4: 184 | } else if ( 185 | // Variant 3: 186 | (a && typeof a === 'object' && !Array.isArray(a)) || 187 | // Variant 4: 188 | (typeof a === 'function') 189 | ) { 190 | b = b || {}; 191 | b.once = true; 192 | this.on(a, b); 193 | 194 | // No matching variant: 195 | } else 196 | throw new Error('Wrong arguments.'); 197 | 198 | return this; 199 | }; 200 | 201 | 202 | /** 203 | * This method unbinds one or more functions from events of the emitter. So, 204 | * these functions will no more be executed when the related events are 205 | * emitted. If the functions were not bound to the events, nothing will 206 | * happen, and no error will be thrown. 207 | * 208 | * Variant 1: 209 | * ********** 210 | * > myEmitter.off('myEvent', myHandler); 211 | * 212 | * @param {string} event The event to unbind the handler from. 213 | * @param {function} handler The function to unbind. 214 | * @return {Emitter} Returns this. 215 | * 216 | * Variant 2: 217 | * ********** 218 | * > myEmitter.off(['myEvent1', 'myEvent2'], myHandler); 219 | * 220 | * @param {array} events The events to unbind the handler from. 221 | * @param {function} handler The function to unbind. 222 | * @return {Emitter} Returns this. 223 | * 224 | * Variant 3: 225 | * ********** 226 | * > myEmitter.off({ 227 | * > myEvent1: myHandler1, 228 | * > myEvent2: myHandler2 229 | * > }); 230 | * 231 | * @param {object} bindings An object containing pairs event / function. 232 | * @return {Emitter} Returns this. 233 | * 234 | * Variant 4: 235 | * ********** 236 | * > myEmitter.off(myHandler); 237 | * 238 | * @param {function} handler The function to unbind from every events. 239 | * @return {Emitter} Returns this. 240 | */ 241 | Emitter.prototype.off = function(events, handler) { 242 | var i, 243 | n, 244 | j, 245 | m, 246 | k, 247 | a, 248 | event, 249 | eArray = typeof events === 'string' ? 250 | [events] : 251 | events; 252 | 253 | if (arguments.length === 1 && typeof eArray === 'function') { 254 | handler = arguments[0]; 255 | 256 | // Handlers bound to events: 257 | for (k in this._handlers) { 258 | a = []; 259 | for (i = 0, n = this._handlers[k].length; i !== n; i += 1) 260 | if (this._handlers[k][i].handler !== handler) 261 | a.push(this._handlers[k][i]); 262 | this._handlers[k] = a; 263 | } 264 | 265 | a = []; 266 | for (i = 0, n = this._handlersAll.length; i !== n; i += 1) 267 | if (this._handlersAll[i].handler !== handler) 268 | a.push(this._handlersAll[i]); 269 | this._handlersAll = a; 270 | } 271 | 272 | else if (arguments.length === 2) { 273 | for (i = 0, n = eArray.length; i !== n; i += 1) { 274 | event = eArray[i]; 275 | if (this._handlers[event]) { 276 | a = []; 277 | for (j = 0, m = this._handlers[event].length; j !== m; j += 1) 278 | if (this._handlers[event][j].handler !== handler) 279 | a.push(this._handlers[event][j]); 280 | 281 | this._handlers[event] = a; 282 | } 283 | 284 | if (this._handlers[event] && this._handlers[event].length === 0) 285 | delete this._handlers[event]; 286 | } 287 | } 288 | 289 | return this; 290 | }; 291 | 292 | 293 | /** 294 | * This method unbinds every handlers attached to every or any events. So, 295 | * these functions will no more be executed when the related events are 296 | * emitted. If the functions were not bound to the events, nothing will 297 | * happen, and no error will be thrown. 298 | * 299 | * Usage: 300 | * ****** 301 | * > myEmitter.unbindAll(); 302 | * 303 | * @return {Emitter} Returns this. 304 | */ 305 | Emitter.prototype.unbindAll = function() { 306 | var k; 307 | 308 | this._handlersAll = []; 309 | for (k in this._handlers) 310 | delete this._handlers[k]; 311 | 312 | return this; 313 | }; 314 | 315 | 316 | /** 317 | * This method emits the specified event(s), and executes every handlers bound 318 | * to the event(s). 319 | * 320 | * Use cases: 321 | * ********** 322 | * > myEmitter.emit('myEvent'); 323 | * > myEmitter.emit('myEvent', myData); 324 | * > myEmitter.emit(['myEvent1', 'myEvent2']); 325 | * > myEmitter.emit(['myEvent1', 'myEvent2'], myData); 326 | * 327 | * @param {string|array} events The event(s) to emit. 328 | * @param {object?} data The data. 329 | * @return {Emitter} Returns this. 330 | */ 331 | Emitter.prototype.emit = function(events, data) { 332 | var i, 333 | n, 334 | j, 335 | m, 336 | z, 337 | a, 338 | event, 339 | child, 340 | handlers, 341 | eventName, 342 | self = this, 343 | eArray = typeof events === 'string' ? 344 | [events] : 345 | events; 346 | 347 | // Check that the emitter is enabled: 348 | if (!this._enabled) 349 | return this; 350 | 351 | data = data === undefined ? {} : data; 352 | 353 | for (i = 0, n = eArray.length; i !== n; i += 1) { 354 | eventName = eArray[i]; 355 | handlers = (this._handlers[eventName] || []).concat(this._handlersAll); 356 | 357 | if (handlers.length) { 358 | event = { 359 | type: eventName, 360 | data: data || {}, 361 | target: this 362 | }; 363 | a = []; 364 | 365 | for (j = 0, m = handlers.length; j !== m; j += 1) { 366 | 367 | // We have to verify that the handler still exists in the array, 368 | // as it might have been mutated already 369 | if ( 370 | ( 371 | this._handlers[eventName] && 372 | this._handlers[eventName].indexOf(handlers[j]) >= 0 373 | ) || 374 | this._handlersAll.indexOf(handlers[j]) >= 0 375 | ) { 376 | handlers[j].handler.call( 377 | 'scope' in handlers[j] ? handlers[j].scope : this, 378 | event 379 | ); 380 | 381 | // Since the listener callback can mutate the _handlers, 382 | // we register the handlers we want to remove, not the ones 383 | // we want to keep 384 | if (handlers[j].once) 385 | a.push(handlers[j]); 386 | } 387 | } 388 | 389 | // Go through handlers to remove 390 | for (z = 0; z < a.length; z++) { 391 | this._handlers[eventName].splice(a.indexOf(a[z]), 1); 392 | } 393 | } 394 | } 395 | 396 | // Events propagation: 397 | for (i = 0, n = this._children.length; i !== n; i += 1) { 398 | child = this._children[i]; 399 | child.emit.apply(child, arguments); 400 | } 401 | 402 | return this; 403 | }; 404 | 405 | 406 | /** 407 | * This method creates a new instance of Emitter and binds it as a child. Here 408 | * is what children do: 409 | * - When the parent emits an event, the children will emit the same later 410 | * - When a child is killed, it is automatically unreferenced from the parent 411 | * - When the parent is killed, all children will be killed as well 412 | * 413 | * @return {Emitter} Returns the fresh new child. 414 | */ 415 | Emitter.prototype.child = function() { 416 | var self = this, 417 | child = new Emitter(); 418 | 419 | child.on('emmett:kill', function() { 420 | if (self._children) 421 | for (var i = 0, l = self._children.length; i < l; i++) 422 | if (self._children[i] === child) { 423 | self._children.splice(i, 1); 424 | break; 425 | } 426 | }); 427 | this._children.push(child); 428 | 429 | return child; 430 | }; 431 | 432 | /** 433 | * This returns an array of handler functions corresponding to the given 434 | * event or every handler functions if an event were not to be given. 435 | * 436 | * @param {?string} event Name of the event. 437 | * @return {Emitter} Returns this. 438 | */ 439 | function mapHandlers(a) { 440 | var i, l, h = []; 441 | 442 | for (i = 0, l = a.length; i < l; i++) 443 | h.push(a[i].handler); 444 | 445 | return h; 446 | } 447 | 448 | Emitter.prototype.listeners = function(event) { 449 | var handlers = [], 450 | k, 451 | i, 452 | l; 453 | 454 | // If no event is passed, we return every handlers 455 | if (!event) { 456 | handlers = mapHandlers(this._handlersAll); 457 | 458 | for (k in this._handlers) 459 | handlers = handlers.concat(mapHandlers(this._handlers[k])); 460 | 461 | // Retrieving handlers per children 462 | for (i = 0, l = this._children.length; i < l; i++) 463 | handlers = handlers.concat(this._children[i].listeners()); 464 | } 465 | 466 | // Else we only retrieve the needed handlers 467 | else { 468 | handlers = mapHandlers(this._handlers[event]); 469 | 470 | // Retrieving handlers per children 471 | for (i = 0, l = this._children.length; i < l; i++) 472 | handlers = handlers.concat(this._children[i].listeners(event)); 473 | } 474 | 475 | return handlers; 476 | }; 477 | 478 | 479 | /** 480 | * This method will first dispatch a "emmett:kill" event, and then unbinds all 481 | * listeners and make it impossible to ever rebind any listener to any event. 482 | */ 483 | Emitter.prototype.kill = function() { 484 | this.emit('emmett:kill'); 485 | 486 | this.unbindAll(); 487 | this._handlers = null; 488 | this._handlersAll = null; 489 | this._enabled = false; 490 | 491 | if (this._children) 492 | for (var i = 0, l = this._children.length; i < l; i++) 493 | this._children[i].kill(); 494 | 495 | this._children = null; 496 | }; 497 | 498 | 499 | /** 500 | * This method disabled the emitter, which means its emit method will do 501 | * nothing. 502 | * 503 | * @return {Emitter} Returns this. 504 | */ 505 | Emitter.prototype.disable = function() { 506 | this._enabled = false; 507 | 508 | return this; 509 | }; 510 | 511 | 512 | /** 513 | * This method enables the emitter. 514 | * 515 | * @return {Emitter} Returns this. 516 | */ 517 | Emitter.prototype.enable = function() { 518 | this._enabled = true; 519 | 520 | return this; 521 | }; 522 | 523 | 524 | /** 525 | * Version: 526 | */ 527 | Emitter.version = '2.1.2'; 528 | 529 | 530 | // Export: 531 | artoo.emitter = Emitter; 532 | }).call(this); 533 | -------------------------------------------------------------------------------- /src/third_party/jquery.simulate.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * jQuery Simulate v1.0.1 - simulate browser mouse and keyboard events 3 | * https://github.com/jquery/jquery-simulate 4 | * 5 | * Copyright jQuery Foundation and other contributors 6 | * Released under the MIT license. 7 | * http://jquery.org/license 8 | */ 9 | 10 | ;(function(undefined) { 11 | 12 | function _simulate($) { 13 | var rkeyEvent = /^key/, 14 | rmouseEvent = /^(?:mouse|contextmenu)|click/; 15 | 16 | $.fn.simulate = function( type, options ) { 17 | return this.each(function() { 18 | new $.simulate( this, type, options ); 19 | }); 20 | }; 21 | 22 | $.simulate = function( elem, type, options ) { 23 | var method = $.camelCase( "simulate-" + type ); 24 | 25 | this.target = elem; 26 | this.options = options; 27 | 28 | if ( this[ method ] ) { 29 | this[ method ](); 30 | } else { 31 | this.simulateEvent( elem, type, options ); 32 | } 33 | }; 34 | 35 | $.extend( $.simulate, { 36 | 37 | keyCode: { 38 | BACKSPACE: 8, 39 | COMMA: 188, 40 | DELETE: 46, 41 | DOWN: 40, 42 | END: 35, 43 | ENTER: 13, 44 | ESCAPE: 27, 45 | HOME: 36, 46 | LEFT: 37, 47 | NUMPAD_ADD: 107, 48 | NUMPAD_DECIMAL: 110, 49 | NUMPAD_DIVIDE: 111, 50 | NUMPAD_ENTER: 108, 51 | NUMPAD_MULTIPLY: 106, 52 | NUMPAD_SUBTRACT: 109, 53 | PAGE_DOWN: 34, 54 | PAGE_UP: 33, 55 | PERIOD: 190, 56 | RIGHT: 39, 57 | SPACE: 32, 58 | TAB: 9, 59 | UP: 38 60 | }, 61 | 62 | buttonCode: { 63 | LEFT: 0, 64 | MIDDLE: 1, 65 | RIGHT: 2 66 | } 67 | }); 68 | 69 | $.extend( $.simulate.prototype, { 70 | 71 | simulateEvent: function( elem, type, options ) { 72 | var event = this.createEvent( type, options ); 73 | this.dispatchEvent( elem, type, event, options ); 74 | }, 75 | 76 | createEvent: function( type, options ) { 77 | if ( rkeyEvent.test( type ) ) { 78 | return this.keyEvent( type, options ); 79 | } 80 | 81 | if ( rmouseEvent.test( type ) ) { 82 | return this.mouseEvent( type, options ); 83 | } 84 | }, 85 | 86 | mouseEvent: function( type, options ) { 87 | var event, eventDoc, doc, body; 88 | options = $.extend({ 89 | bubbles: true, 90 | cancelable: (type !== "mousemove"), 91 | view: window, 92 | detail: 0, 93 | screenX: 0, 94 | screenY: 0, 95 | clientX: 1, 96 | clientY: 1, 97 | ctrlKey: false, 98 | altKey: false, 99 | shiftKey: false, 100 | metaKey: false, 101 | button: 0, 102 | relatedTarget: undefined 103 | }, options ); 104 | 105 | if ( document.createEvent ) { 106 | event = document.createEvent( "MouseEvents" ); 107 | event.initMouseEvent( type, options.bubbles, options.cancelable, 108 | options.view, options.detail, 109 | options.screenX, options.screenY, options.clientX, options.clientY, 110 | options.ctrlKey, options.altKey, options.shiftKey, options.metaKey, 111 | options.button, options.relatedTarget || document.body.parentNode ); 112 | 113 | // IE 9+ creates events with pageX and pageY set to 0. 114 | // Trying to modify the properties throws an error, 115 | // so we define getters to return the correct values. 116 | if ( event.pageX === 0 && event.pageY === 0 && Object.defineProperty ) { 117 | eventDoc = event.relatedTarget.ownerDocument || document; 118 | doc = eventDoc.documentElement; 119 | body = eventDoc.body; 120 | 121 | Object.defineProperty( event, "pageX", { 122 | get: function() { 123 | return options.clientX + 124 | ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) - 125 | ( doc && doc.clientLeft || body && body.clientLeft || 0 ); 126 | } 127 | }); 128 | Object.defineProperty( event, "pageY", { 129 | get: function() { 130 | return options.clientY + 131 | ( doc && doc.scrollTop || body && body.scrollTop || 0 ) - 132 | ( doc && doc.clientTop || body && body.clientTop || 0 ); 133 | } 134 | }); 135 | } 136 | } else if ( document.createEventObject ) { 137 | event = document.createEventObject(); 138 | $.extend( event, options ); 139 | // standards event.button uses constants defined here: http://msdn.microsoft.com/en-us/library/ie/ff974877(v=vs.85).aspx 140 | // old IE event.button uses constants defined here: http://msdn.microsoft.com/en-us/library/ie/ms533544(v=vs.85).aspx 141 | // so we actually need to map the standard back to oldIE 142 | event.button = { 143 | 0: 1, 144 | 1: 4, 145 | 2: 2 146 | }[ event.button ] || ( event.button === -1 ? 0 : event.button ); 147 | } 148 | 149 | return event; 150 | }, 151 | 152 | keyEvent: function( type, options ) { 153 | var event; 154 | options = $.extend({ 155 | bubbles: true, 156 | cancelable: true, 157 | view: window, 158 | ctrlKey: false, 159 | altKey: false, 160 | shiftKey: false, 161 | metaKey: false, 162 | keyCode: 0, 163 | charCode: undefined 164 | }, options ); 165 | 166 | if ( document.createEvent ) { 167 | try { 168 | event = document.createEvent( "KeyEvents" ); 169 | event.initKeyEvent( type, options.bubbles, options.cancelable, options.view, 170 | options.ctrlKey, options.altKey, options.shiftKey, options.metaKey, 171 | options.keyCode, options.charCode ); 172 | // initKeyEvent throws an exception in WebKit 173 | // see: http://stackoverflow.com/questions/6406784/initkeyevent-keypress-only-works-in-firefox-need-a-cross-browser-solution 174 | // and also https://bugs.webkit.org/show_bug.cgi?id=13368 175 | // fall back to a generic event until we decide to implement initKeyboardEvent 176 | } catch( err ) { 177 | event = document.createEvent( "Events" ); 178 | event.initEvent( type, options.bubbles, options.cancelable ); 179 | $.extend( event, { 180 | view: options.view, 181 | ctrlKey: options.ctrlKey, 182 | altKey: options.altKey, 183 | shiftKey: options.shiftKey, 184 | metaKey: options.metaKey, 185 | keyCode: options.keyCode, 186 | charCode: options.charCode 187 | }); 188 | } 189 | } else if ( document.createEventObject ) { 190 | event = document.createEventObject(); 191 | $.extend( event, options ); 192 | } 193 | 194 | if ( !!/msie [\w.]+/.exec( navigator.userAgent.toLowerCase() ) || (({}).toString.call( window.opera ) === "[object Opera]") ) { 195 | event.keyCode = (options.charCode > 0) ? options.charCode : options.keyCode; 196 | event.charCode = undefined; 197 | } 198 | 199 | return event; 200 | }, 201 | 202 | dispatchEvent: function( elem, type, event ) { 203 | if ( elem[ type ] ) { 204 | elem[ type ](); 205 | } else if ( elem.dispatchEvent ) { 206 | elem.dispatchEvent( event ); 207 | } else if ( elem.fireEvent ) { 208 | elem.fireEvent( "on" + type, event ); 209 | } 210 | }, 211 | 212 | simulateFocus: function() { 213 | var focusinEvent, 214 | triggered = false, 215 | element = $( this.target ); 216 | 217 | function trigger() { 218 | triggered = true; 219 | } 220 | 221 | element.bind( "focus", trigger ); 222 | element[ 0 ].focus(); 223 | 224 | if ( !triggered ) { 225 | focusinEvent = $.Event( "focusin" ); 226 | focusinEvent.preventDefault(); 227 | element.trigger( focusinEvent ); 228 | element.triggerHandler( "focus" ); 229 | } 230 | element.unbind( "focus", trigger ); 231 | }, 232 | 233 | simulateBlur: function() { 234 | var focusoutEvent, 235 | triggered = false, 236 | element = $( this.target ); 237 | 238 | function trigger() { 239 | triggered = true; 240 | } 241 | 242 | element.bind( "blur", trigger ); 243 | element[ 0 ].blur(); 244 | 245 | // blur events are async in IE 246 | setTimeout(function() { 247 | // IE won't let the blur occur if the window is inactive 248 | if ( element[ 0 ].ownerDocument.activeElement === element[ 0 ] ) { 249 | element[ 0 ].ownerDocument.body.focus(); 250 | } 251 | 252 | // Firefox won't trigger events if the window is inactive 253 | // IE doesn't trigger events if we had to manually focus the body 254 | if ( !triggered ) { 255 | focusoutEvent = $.Event( "focusout" ); 256 | focusoutEvent.preventDefault(); 257 | element.trigger( focusoutEvent ); 258 | element.triggerHandler( "blur" ); 259 | } 260 | element.unbind( "blur", trigger ); 261 | }, 1 ); 262 | } 263 | }); 264 | 265 | 266 | 267 | /** complex events **/ 268 | 269 | function findCenter( elem ) { 270 | var offset, 271 | document = $( elem.ownerDocument ); 272 | elem = $( elem ); 273 | offset = elem.offset(); 274 | 275 | return { 276 | x: offset.left + elem.outerWidth() / 2 - document.scrollLeft(), 277 | y: offset.top + elem.outerHeight() / 2 - document.scrollTop() 278 | }; 279 | } 280 | 281 | function findCorner( elem ) { 282 | var offset, 283 | document = $( elem.ownerDocument ); 284 | elem = $( elem ); 285 | offset = elem.offset(); 286 | 287 | return { 288 | x: offset.left - document.scrollLeft(), 289 | y: offset.top - document.scrollTop() 290 | }; 291 | } 292 | 293 | $.extend( $.simulate.prototype, { 294 | simulateDrag: function() { 295 | var i = 0, 296 | target = this.target, 297 | eventDoc = target.ownerDocument, 298 | options = this.options, 299 | center = options.handle === "corner" ? findCorner( target ) : findCenter( target ), 300 | x = Math.floor( center.x ), 301 | y = Math.floor( center.y ), 302 | coord = { clientX: x, clientY: y }, 303 | dx = options.dx || ( options.x !== undefined ? options.x - x : 0 ), 304 | dy = options.dy || ( options.y !== undefined ? options.y - y : 0 ), 305 | moves = options.moves || 3; 306 | 307 | this.simulateEvent( target, "mousedown", coord ); 308 | 309 | for ( ; i < moves ; i++ ) { 310 | x += dx / moves; 311 | y += dy / moves; 312 | 313 | coord = { 314 | clientX: Math.round( x ), 315 | clientY: Math.round( y ) 316 | }; 317 | 318 | this.simulateEvent( eventDoc, "mousemove", coord ); 319 | } 320 | 321 | if ( $.contains( eventDoc, target ) ) { 322 | this.simulateEvent( target, "mouseup", coord ); 323 | this.simulateEvent( target, "click", coord ); 324 | } else { 325 | this.simulateEvent( eventDoc, "mouseup", coord ); 326 | } 327 | } 328 | }); 329 | } 330 | 331 | // Exporting 332 | artoo.jquery.plugins.push(_simulate); 333 | }).call(this); 334 | -------------------------------------------------------------------------------- /test/endpoint.js: -------------------------------------------------------------------------------- 1 | /** 2 | * artoo node unit tests endpoint 3 | * =============================== 4 | * 5 | * Requiring the node.js test suites. 6 | */ 7 | 8 | require('./suites/node.test.js'); 9 | require('./suites/node.scrape.test.js'); 10 | -------------------------------------------------------------------------------- /test/helpers.js: -------------------------------------------------------------------------------- 1 | ;(function(undefined) { 2 | 3 | /** 4 | * Some unit tests helpers 5 | * ======================== 6 | * 7 | */ 8 | 9 | // Selectors 10 | var $doms = $('#artoo-doms'); 11 | 12 | // Generic helpers 13 | this.helpers = { 14 | 15 | // Returns some html and append it to an invible DOM node for testing 16 | fetchHTMLResource: function(name, cb) { 17 | 18 | // Appending a new dom 19 | var $newDom = $('
'); 20 | $doms.append($newDom); 21 | 22 | // Loading from resources. 23 | $newDom.load('resources/' + name + '.html', 24 | function() { 25 | cb('#' + name); 26 | }); 27 | } 28 | }; 29 | 30 | // Mockup asynchronous data store 31 | this.MockupAsynchronousStore = function() { 32 | var self = this; 33 | 34 | // Properties 35 | this.index = 0; 36 | this.store = {}; 37 | this.calls = {}; 38 | 39 | // Methods 40 | this._send = function(header, data) { 41 | window.postMessage({ 42 | id: ++this.index, 43 | header: header, 44 | body: data 45 | }, '*'); 46 | }; 47 | 48 | this.send = function(action, params) { 49 | var promise = $.Deferred(); 50 | 51 | // Sending message 52 | self._send('store', {action: action, params: params}); 53 | 54 | // Registering call 55 | self.calls[self.index] = promise; 56 | 57 | return promise; 58 | }; 59 | 60 | // Receptors 61 | window.addEventListener('message', function(e) { 62 | var body = e.data.body; 63 | 64 | // Acting 65 | switch (body.action) { 66 | case 'set': 67 | self.store[body.params.key] = body.params.value; 68 | self.calls[e.data.id].resolve(); 69 | break; 70 | case 'get': 71 | self.calls[e.data.id].resolve( 72 | self.store[body.params.key]); 73 | break; 74 | case 'getAll': 75 | self.calls[e.data.id].resolve( 76 | self.store); 77 | break; 78 | case 'keys': 79 | self.calls[e.data.id].resolve(Object.keys(self.store)); 80 | break; 81 | case 'remove': 82 | delete self.store[body.params.key]; 83 | self.calls[e.data.id].resolve(); 84 | break; 85 | case 'removeAll': 86 | self.store = {}; 87 | self.calls[e.data.id].resolve(); 88 | break; 89 | default: 90 | throw Error('mockup: wrong action: "' + body.action + '".'); 91 | } 92 | 93 | // Cleaning call 94 | delete self.calls[e.data.id]; 95 | }, false); 96 | }; 97 | }).call(this); 98 | -------------------------------------------------------------------------------- /test/lib/jquery.mockjax.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * MockJax - jQuery Plugin to Mock Ajax requests 3 | * 4 | * Version: 1.5.3 5 | * Released: 6 | * Home: http://github.com/appendto/jquery-mockjax 7 | * Author: Jonathan Sharp (http://jdsharp.com) 8 | * License: MIT,GPL 9 | * 10 | * Copyright (c) 2011 appendTo LLC. 11 | * Dual licensed under the MIT or GPL licenses. 12 | * http://appendto.com/open-source-licenses 13 | */ 14 | (function($) { 15 | var _ajax = $.ajax, 16 | mockHandlers = [], 17 | mockedAjaxCalls = [], 18 | CALLBACK_REGEX = /=\?(&|$)/, 19 | jsc = (new Date()).getTime(); 20 | 21 | 22 | // Parse the given XML string. 23 | function parseXML(xml) { 24 | if ( window.DOMParser == undefined && window.ActiveXObject ) { 25 | DOMParser = function() { }; 26 | DOMParser.prototype.parseFromString = function( xmlString ) { 27 | var doc = new ActiveXObject('Microsoft.XMLDOM'); 28 | doc.async = 'false'; 29 | doc.loadXML( xmlString ); 30 | return doc; 31 | }; 32 | } 33 | 34 | try { 35 | var xmlDoc = ( new DOMParser() ).parseFromString( xml, 'text/xml' ); 36 | if ( $.isXMLDoc( xmlDoc ) ) { 37 | var err = $('parsererror', xmlDoc); 38 | if ( err.length == 1 ) { 39 | throw('Error: ' + $(xmlDoc).text() ); 40 | } 41 | } else { 42 | throw('Unable to parse XML'); 43 | } 44 | return xmlDoc; 45 | } catch( e ) { 46 | var msg = ( e.name == undefined ? e : e.name + ': ' + e.message ); 47 | $(document).trigger('xmlParseError', [ msg ]); 48 | return undefined; 49 | } 50 | } 51 | 52 | // Trigger a jQuery event 53 | function trigger(s, type, args) { 54 | (s.context ? $(s.context) : $.event).trigger(type, args); 55 | } 56 | 57 | // Check if the data field on the mock handler and the request match. This 58 | // can be used to restrict a mock handler to being used only when a certain 59 | // set of data is passed to it. 60 | function isMockDataEqual( mock, live ) { 61 | var identical = true; 62 | // Test for situations where the data is a querystring (not an object) 63 | if (typeof live === 'string') { 64 | // Querystring may be a regex 65 | return $.isFunction( mock.test ) ? mock.test(live) : mock == live; 66 | } 67 | $.each(mock, function(k) { 68 | if ( live[k] === undefined ) { 69 | identical = false; 70 | return identical; 71 | } else { 72 | // This will allow to compare Arrays 73 | if ( typeof live[k] === 'object' && live[k] !== null ) { 74 | identical = identical && isMockDataEqual(mock[k], live[k]); 75 | } else { 76 | if ( mock[k] && $.isFunction( mock[k].test ) ) { 77 | identical = identical && mock[k].test(live[k]); 78 | } else { 79 | identical = identical && ( mock[k] == live[k] ); 80 | } 81 | } 82 | } 83 | }); 84 | 85 | return identical; 86 | } 87 | 88 | // See if a mock handler property matches the default settings 89 | function isDefaultSetting(handler, property) { 90 | return handler[property] === $.mockjaxSettings[property]; 91 | } 92 | 93 | // Check the given handler should mock the given request 94 | function getMockForRequest( handler, requestSettings ) { 95 | // If the mock was registered with a function, let the function decide if we 96 | // want to mock this request 97 | if ( $.isFunction(handler) ) { 98 | return handler( requestSettings ); 99 | } 100 | 101 | // Inspect the URL of the request and check if the mock handler's url 102 | // matches the url for this ajax request 103 | if ( $.isFunction(handler.url.test) ) { 104 | // The user provided a regex for the url, test it 105 | if ( !handler.url.test( requestSettings.url ) ) { 106 | return null; 107 | } 108 | } else { 109 | // Look for a simple wildcard '*' or a direct URL match 110 | var star = handler.url.indexOf('*'); 111 | if (handler.url !== requestSettings.url && star === -1 || 112 | !new RegExp(handler.url.replace(/[-[\]{}()+?.,\\^$|#\s]/g, "\\$&").replace(/\*/g, '.+')).test(requestSettings.url)) { 113 | return null; 114 | } 115 | } 116 | 117 | // Inspect the data submitted in the request (either POST body or GET query string) 118 | if ( handler.data && requestSettings.data ) { 119 | if ( !isMockDataEqual(handler.data, requestSettings.data) ) { 120 | // They're not identical, do not mock this request 121 | return null; 122 | } 123 | } 124 | // Inspect the request type 125 | if ( handler && handler.type && 126 | handler.type.toLowerCase() != requestSettings.type.toLowerCase() ) { 127 | // The request type doesn't match (GET vs. POST) 128 | return null; 129 | } 130 | 131 | return handler; 132 | } 133 | 134 | // Process the xhr objects send operation 135 | function _xhrSend(mockHandler, requestSettings, origSettings) { 136 | 137 | // This is a substitute for < 1.4 which lacks $.proxy 138 | var process = (function(that) { 139 | return function() { 140 | return (function() { 141 | var onReady; 142 | 143 | // The request has returned 144 | this.status = mockHandler.status; 145 | this.statusText = mockHandler.statusText; 146 | this.readyState = 4; 147 | 148 | // We have an executable function, call it to give 149 | // the mock handler a chance to update it's data 150 | if ( $.isFunction(mockHandler.response) ) { 151 | mockHandler.response(origSettings); 152 | } 153 | // Copy over our mock to our xhr object before passing control back to 154 | // jQuery's onreadystatechange callback 155 | if ( requestSettings.dataType == 'json' && ( typeof mockHandler.responseText == 'object' ) ) { 156 | this.responseText = JSON.stringify(mockHandler.responseText); 157 | } else if ( requestSettings.dataType == 'xml' ) { 158 | if ( typeof mockHandler.responseXML == 'string' ) { 159 | this.responseXML = parseXML(mockHandler.responseXML); 160 | //in jQuery 1.9.1+, responseXML is processed differently and relies on responseText 161 | this.responseText = mockHandler.responseXML; 162 | } else { 163 | this.responseXML = mockHandler.responseXML; 164 | } 165 | } else { 166 | this.responseText = mockHandler.responseText; 167 | } 168 | if( typeof mockHandler.status == 'number' || typeof mockHandler.status == 'string' ) { 169 | this.status = mockHandler.status; 170 | } 171 | if( typeof mockHandler.statusText === "string") { 172 | this.statusText = mockHandler.statusText; 173 | } 174 | // jQuery 2.0 renamed onreadystatechange to onload 175 | onReady = this.onreadystatechange || this.onload; 176 | 177 | // jQuery < 1.4 doesn't have onreadystate change for xhr 178 | if ( $.isFunction( onReady ) ) { 179 | if( mockHandler.isTimeout) { 180 | this.status = -1; 181 | } 182 | onReady.call( this, mockHandler.isTimeout ? 'timeout' : undefined ); 183 | } else if ( mockHandler.isTimeout ) { 184 | // Fix for 1.3.2 timeout to keep success from firing. 185 | this.status = -1; 186 | } 187 | }).apply(that); 188 | }; 189 | })(this); 190 | 191 | if ( mockHandler.proxy ) { 192 | // We're proxying this request and loading in an external file instead 193 | _ajax({ 194 | global: false, 195 | url: mockHandler.proxy, 196 | type: mockHandler.proxyType, 197 | data: mockHandler.data, 198 | dataType: requestSettings.dataType === "script" ? "text/plain" : requestSettings.dataType, 199 | complete: function(xhr) { 200 | mockHandler.responseXML = xhr.responseXML; 201 | mockHandler.responseText = xhr.responseText; 202 | // Don't override the handler status/statusText if it's specified by the config 203 | if (isDefaultSetting(mockHandler, 'status')) { 204 | mockHandler.status = xhr.status; 205 | } 206 | if (isDefaultSetting(mockHandler, 'statusText')) { 207 | mockHandler.statusText = xhr.statusText; 208 | } 209 | 210 | this.responseTimer = setTimeout(process, mockHandler.responseTime || 0); 211 | } 212 | }); 213 | } else { 214 | // type == 'POST' || 'GET' || 'DELETE' 215 | if ( requestSettings.async === false ) { 216 | // TODO: Blocking delay 217 | process(); 218 | } else { 219 | this.responseTimer = setTimeout(process, mockHandler.responseTime || 50); 220 | } 221 | } 222 | } 223 | 224 | // Construct a mocked XHR Object 225 | function xhr(mockHandler, requestSettings, origSettings, origHandler) { 226 | // Extend with our default mockjax settings 227 | mockHandler = $.extend(true, {}, $.mockjaxSettings, mockHandler); 228 | 229 | if (typeof mockHandler.headers === 'undefined') { 230 | mockHandler.headers = {}; 231 | } 232 | if ( mockHandler.contentType ) { 233 | mockHandler.headers['content-type'] = mockHandler.contentType; 234 | } 235 | 236 | return { 237 | status: mockHandler.status, 238 | statusText: mockHandler.statusText, 239 | readyState: 1, 240 | open: function() { }, 241 | send: function() { 242 | origHandler.fired = true; 243 | _xhrSend.call(this, mockHandler, requestSettings, origSettings); 244 | }, 245 | abort: function() { 246 | clearTimeout(this.responseTimer); 247 | }, 248 | setRequestHeader: function(header, value) { 249 | mockHandler.headers[header] = value; 250 | }, 251 | getResponseHeader: function(header) { 252 | // 'Last-modified', 'Etag', 'content-type' are all checked by jQuery 253 | if ( mockHandler.headers && mockHandler.headers[header] ) { 254 | // Return arbitrary headers 255 | return mockHandler.headers[header]; 256 | } else if ( header.toLowerCase() == 'last-modified' ) { 257 | return mockHandler.lastModified || (new Date()).toString(); 258 | } else if ( header.toLowerCase() == 'etag' ) { 259 | return mockHandler.etag || ''; 260 | } else if ( header.toLowerCase() == 'content-type' ) { 261 | return mockHandler.contentType || 'text/plain'; 262 | } 263 | }, 264 | getAllResponseHeaders: function() { 265 | var headers = ''; 266 | $.each(mockHandler.headers, function(k, v) { 267 | headers += k + ': ' + v + "\n"; 268 | }); 269 | return headers; 270 | } 271 | }; 272 | } 273 | 274 | // Process a JSONP mock request. 275 | function processJsonpMock( requestSettings, mockHandler, origSettings ) { 276 | // Handle JSONP Parameter Callbacks, we need to replicate some of the jQuery core here 277 | // because there isn't an easy hook for the cross domain script tag of jsonp 278 | 279 | processJsonpUrl( requestSettings ); 280 | 281 | requestSettings.dataType = "json"; 282 | if(requestSettings.data && CALLBACK_REGEX.test(requestSettings.data) || CALLBACK_REGEX.test(requestSettings.url)) { 283 | createJsonpCallback(requestSettings, mockHandler, origSettings); 284 | 285 | // We need to make sure 286 | // that a JSONP style response is executed properly 287 | 288 | var rurl = /^(\w+:)?\/\/([^\/?#]+)/, 289 | parts = rurl.exec( requestSettings.url ), 290 | remote = parts && (parts[1] && parts[1] !== location.protocol || parts[2] !== location.host); 291 | 292 | requestSettings.dataType = "script"; 293 | if(requestSettings.type.toUpperCase() === "GET" && remote ) { 294 | var newMockReturn = processJsonpRequest( requestSettings, mockHandler, origSettings ); 295 | 296 | // Check if we are supposed to return a Deferred back to the mock call, or just 297 | // signal success 298 | if(newMockReturn) { 299 | return newMockReturn; 300 | } else { 301 | return true; 302 | } 303 | } 304 | } 305 | return null; 306 | } 307 | 308 | // Append the required callback parameter to the end of the request URL, for a JSONP request 309 | function processJsonpUrl( requestSettings ) { 310 | if ( requestSettings.type.toUpperCase() === "GET" ) { 311 | if ( !CALLBACK_REGEX.test( requestSettings.url ) ) { 312 | requestSettings.url += (/\?/.test( requestSettings.url ) ? "&" : "?") + 313 | (requestSettings.jsonp || "callback") + "=?"; 314 | } 315 | } else if ( !requestSettings.data || !CALLBACK_REGEX.test(requestSettings.data) ) { 316 | requestSettings.data = (requestSettings.data ? requestSettings.data + "&" : "") + (requestSettings.jsonp || "callback") + "=?"; 317 | } 318 | } 319 | 320 | // Process a JSONP request by evaluating the mocked response text 321 | function processJsonpRequest( requestSettings, mockHandler, origSettings ) { 322 | // Synthesize the mock request for adding a script tag 323 | var callbackContext = origSettings && origSettings.context || requestSettings, 324 | newMock = null; 325 | 326 | 327 | // If the response handler on the moock is a function, call it 328 | if ( mockHandler.response && $.isFunction(mockHandler.response) ) { 329 | mockHandler.response(origSettings); 330 | } else { 331 | 332 | // Evaluate the responseText javascript in a global context 333 | if( typeof mockHandler.responseText === 'object' ) { 334 | $.globalEval( '(' + JSON.stringify( mockHandler.responseText ) + ')'); 335 | } else { 336 | $.globalEval( '(' + mockHandler.responseText + ')'); 337 | } 338 | } 339 | 340 | // Successful response 341 | jsonpSuccess( requestSettings, callbackContext, mockHandler ); 342 | jsonpComplete( requestSettings, callbackContext, mockHandler ); 343 | 344 | // If we are running under jQuery 1.5+, return a deferred object 345 | if($.Deferred){ 346 | newMock = new $.Deferred(); 347 | if(typeof mockHandler.responseText == "object"){ 348 | newMock.resolveWith( callbackContext, [mockHandler.responseText] ); 349 | } 350 | else{ 351 | newMock.resolveWith( callbackContext, [$.parseJSON( mockHandler.responseText )] ); 352 | } 353 | } 354 | return newMock; 355 | } 356 | 357 | 358 | // Create the required JSONP callback function for the request 359 | function createJsonpCallback( requestSettings, mockHandler, origSettings ) { 360 | var callbackContext = origSettings && origSettings.context || requestSettings; 361 | var jsonp = requestSettings.jsonpCallback || ("jsonp" + jsc++); 362 | 363 | // Replace the =? sequence both in the query string and the data 364 | if ( requestSettings.data ) { 365 | requestSettings.data = (requestSettings.data + "").replace(CALLBACK_REGEX, "=" + jsonp + "$1"); 366 | } 367 | 368 | requestSettings.url = requestSettings.url.replace(CALLBACK_REGEX, "=" + jsonp + "$1"); 369 | 370 | 371 | // Handle JSONP-style loading 372 | window[ jsonp ] = window[ jsonp ] || function( tmp ) { 373 | data = tmp; 374 | jsonpSuccess( requestSettings, callbackContext, mockHandler ); 375 | jsonpComplete( requestSettings, callbackContext, mockHandler ); 376 | // Garbage collect 377 | window[ jsonp ] = undefined; 378 | 379 | try { 380 | delete window[ jsonp ]; 381 | } catch(e) {} 382 | 383 | if ( head ) { 384 | head.removeChild( script ); 385 | } 386 | }; 387 | } 388 | 389 | // The JSONP request was successful 390 | function jsonpSuccess(requestSettings, callbackContext, mockHandler) { 391 | // If a local callback was specified, fire it and pass it the data 392 | if ( requestSettings.success ) { 393 | requestSettings.success.call( callbackContext, mockHandler.responseText || "", status, {} ); 394 | } 395 | 396 | // Fire the global callback 397 | if ( requestSettings.global ) { 398 | trigger(requestSettings, "ajaxSuccess", [{}, requestSettings] ); 399 | } 400 | } 401 | 402 | // The JSONP request was completed 403 | function jsonpComplete(requestSettings, callbackContext) { 404 | // Process result 405 | if ( requestSettings.complete ) { 406 | requestSettings.complete.call( callbackContext, {} , status ); 407 | } 408 | 409 | // The request was completed 410 | if ( requestSettings.global ) { 411 | trigger( "ajaxComplete", [{}, requestSettings] ); 412 | } 413 | 414 | // Handle the global AJAX counter 415 | if ( requestSettings.global && ! --$.active ) { 416 | $.event.trigger( "ajaxStop" ); 417 | } 418 | } 419 | 420 | 421 | // The core $.ajax replacement. 422 | function handleAjax( url, origSettings ) { 423 | var mockRequest, requestSettings, mockHandler; 424 | 425 | // If url is an object, simulate pre-1.5 signature 426 | if ( typeof url === "object" ) { 427 | origSettings = url; 428 | url = undefined; 429 | } else { 430 | // work around to support 1.5 signature 431 | origSettings.url = url; 432 | } 433 | 434 | // Extend the original settings for the request 435 | requestSettings = $.extend(true, {}, $.ajaxSettings, origSettings); 436 | 437 | // Iterate over our mock handlers (in registration order) until we find 438 | // one that is willing to intercept the request 439 | for(var k = 0; k < mockHandlers.length; k++) { 440 | if ( !mockHandlers[k] ) { 441 | continue; 442 | } 443 | 444 | mockHandler = getMockForRequest( mockHandlers[k], requestSettings ); 445 | if(!mockHandler) { 446 | // No valid mock found for this request 447 | continue; 448 | } 449 | 450 | mockedAjaxCalls.push(requestSettings); 451 | 452 | // If logging is enabled, log the mock to the console 453 | $.mockjaxSettings.log( mockHandler, requestSettings ); 454 | 455 | 456 | if ( requestSettings.dataType === "jsonp" ) { 457 | if ((mockRequest = processJsonpMock( requestSettings, mockHandler, origSettings ))) { 458 | // This mock will handle the JSONP request 459 | return mockRequest; 460 | } 461 | } 462 | 463 | 464 | // Removed to fix #54 - keep the mocking data object intact 465 | //mockHandler.data = requestSettings.data; 466 | 467 | mockHandler.cache = requestSettings.cache; 468 | mockHandler.timeout = requestSettings.timeout; 469 | mockHandler.global = requestSettings.global; 470 | 471 | copyUrlParameters(mockHandler, origSettings); 472 | 473 | (function(mockHandler, requestSettings, origSettings, origHandler) { 474 | mockRequest = _ajax.call($, $.extend(true, {}, origSettings, { 475 | // Mock the XHR object 476 | xhr: function() { return xhr( mockHandler, requestSettings, origSettings, origHandler ); } 477 | })); 478 | })(mockHandler, requestSettings, origSettings, mockHandlers[k]); 479 | 480 | return mockRequest; 481 | } 482 | 483 | // We don't have a mock request 484 | if($.mockjaxSettings.throwUnmocked === true) { 485 | throw('AJAX not mocked: ' + origSettings.url); 486 | } 487 | else { // trigger a normal request 488 | return _ajax.apply($, [origSettings]); 489 | } 490 | } 491 | 492 | /** 493 | * Copies URL parameter values if they were captured by a regular expression 494 | * @param {Object} mockHandler 495 | * @param {Object} origSettings 496 | */ 497 | function copyUrlParameters(mockHandler, origSettings) { 498 | //parameters aren't captured if the URL isn't a RegExp 499 | if (!(mockHandler.url instanceof RegExp)) { 500 | return; 501 | } 502 | //if no URL params were defined on the handler, don't attempt a capture 503 | if (!mockHandler.hasOwnProperty('urlParams')) { 504 | return; 505 | } 506 | var captures = mockHandler.url.exec(origSettings.url); 507 | //the whole RegExp match is always the first value in the capture results 508 | if (captures.length === 1) { 509 | return; 510 | } 511 | captures.shift(); 512 | //use handler params as keys and capture resuts as values 513 | var i = 0, 514 | capturesLength = captures.length, 515 | paramsLength = mockHandler.urlParams.length, 516 | //in case the number of params specified is less than actual captures 517 | maxIterations = Math.min(capturesLength, paramsLength), 518 | paramValues = {}; 519 | for (i; i < maxIterations; i++) { 520 | var key = mockHandler.urlParams[i]; 521 | paramValues[key] = captures[i]; 522 | } 523 | origSettings.urlParams = paramValues; 524 | } 525 | 526 | 527 | // Public 528 | 529 | $.extend({ 530 | ajax: handleAjax 531 | }); 532 | 533 | $.mockjaxSettings = { 534 | //url: null, 535 | //type: 'GET', 536 | log: function( mockHandler, requestSettings ) { 537 | if ( mockHandler.logging === false || 538 | ( typeof mockHandler.logging === 'undefined' && $.mockjaxSettings.logging === false ) ) { 539 | return; 540 | } 541 | if ( window.console && console.log ) { 542 | var message = 'MOCK ' + requestSettings.type.toUpperCase() + ': ' + requestSettings.url; 543 | var request = $.extend({}, requestSettings); 544 | 545 | if (typeof console.log === 'function') { 546 | console.log(message, request); 547 | } else { 548 | try { 549 | console.log( message + ' ' + JSON.stringify(request) ); 550 | } catch (e) { 551 | console.log(message); 552 | } 553 | } 554 | } 555 | }, 556 | logging: true, 557 | status: 200, 558 | statusText: "OK", 559 | responseTime: 500, 560 | isTimeout: false, 561 | throwUnmocked: false, 562 | contentType: 'text/plain', 563 | response: '', 564 | responseText: '', 565 | responseXML: '', 566 | proxy: '', 567 | proxyType: 'GET', 568 | 569 | lastModified: null, 570 | etag: '', 571 | headers: { 572 | etag: 'IJF@H#@923uf8023hFO@I#H#', 573 | 'content-type' : 'text/plain' 574 | } 575 | }; 576 | 577 | $.mockjax = function(settings) { 578 | var i = mockHandlers.length; 579 | mockHandlers[i] = settings; 580 | return i; 581 | }; 582 | $.mockjaxClear = function(i) { 583 | if ( arguments.length == 1 ) { 584 | mockHandlers[i] = null; 585 | } else { 586 | mockHandlers = []; 587 | } 588 | mockedAjaxCalls = []; 589 | }; 590 | $.mockjax.handler = function(i) { 591 | if ( arguments.length == 1 ) { 592 | return mockHandlers[i]; 593 | } 594 | }; 595 | $.mockjax.mockedAjaxCalls = function() { 596 | return mockedAjaxCalls; 597 | }; 598 | })(jQuery); 599 | -------------------------------------------------------------------------------- /test/lib/jquery.xmldom.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * jQuery xmlDOM Plugin v1.0 3 | * http://outwestmedia.com/jquery-plugins/xmldom/ 4 | * 5 | * Released: 2009-04-06 6 | * Version: 1.0 7 | * 8 | * Copyright (c) 2009 Jonathan Sharp, Out West Media LLC. 9 | * Dual licensed under the MIT and GPL licenses. 10 | * http://docs.jquery.com/License 11 | */ 12 | (function($) { 13 | // IE DOMParser wrapper 14 | if ( window['DOMParser'] == undefined && window.ActiveXObject ) { 15 | DOMParser = function() { }; 16 | DOMParser.prototype.parseFromString = function( xmlString ) { 17 | var doc = new ActiveXObject('Microsoft.XMLDOM'); 18 | doc.async = 'false'; 19 | doc.loadXML( xmlString ); 20 | return doc; 21 | }; 22 | } 23 | 24 | $.xmlDOM = function(xml, onErrorFn) { 25 | try { 26 | var xmlDoc = ( new DOMParser() ).parseFromString( xml, 'text/xml' ); 27 | if ( $.isXMLDoc( xmlDoc ) ) { 28 | var err = $('parsererror', xmlDoc); 29 | if ( err.length == 1 ) { 30 | throw('Error: ' + $(xmlDoc).text() ); 31 | } 32 | } else { 33 | throw('Unable to parse XML'); 34 | } 35 | } catch( e ) { 36 | var msg = ( e.name == undefined ? e : e.name + ': ' + e.message ); 37 | if ( $.isFunction( onErrorFn ) ) { 38 | onErrorFn( msg ); 39 | } else { 40 | $(document).trigger('xmlParseError', [ msg ]); 41 | } 42 | return $([]); 43 | } 44 | return $( xmlDoc ); 45 | }; 46 | })(jQuery); -------------------------------------------------------------------------------- /test/lib/mocha.css: -------------------------------------------------------------------------------- 1 | @charset "utf-8"; 2 | 3 | body { 4 | margin:0; 5 | } 6 | 7 | #mocha { 8 | font: 20px/1.5 "Helvetica Neue", Helvetica, Arial, sans-serif; 9 | margin: 60px 50px; 10 | } 11 | 12 | #mocha ul, 13 | #mocha li { 14 | margin: 0; 15 | padding: 0; 16 | } 17 | 18 | #mocha ul { 19 | list-style: none; 20 | } 21 | 22 | #mocha h1, 23 | #mocha h2 { 24 | margin: 0; 25 | } 26 | 27 | #mocha h1 { 28 | margin-top: 15px; 29 | font-size: 1em; 30 | font-weight: 200; 31 | } 32 | 33 | #mocha h1 a { 34 | text-decoration: none; 35 | color: inherit; 36 | } 37 | 38 | #mocha h1 a:hover { 39 | text-decoration: underline; 40 | } 41 | 42 | #mocha .suite .suite h1 { 43 | margin-top: 0; 44 | font-size: .8em; 45 | } 46 | 47 | #mocha .hidden { 48 | display: none; 49 | } 50 | 51 | #mocha h2 { 52 | font-size: 12px; 53 | font-weight: normal; 54 | cursor: pointer; 55 | } 56 | 57 | #mocha .suite { 58 | margin-left: 15px; 59 | } 60 | 61 | #mocha .test { 62 | margin-left: 15px; 63 | overflow: hidden; 64 | } 65 | 66 | #mocha .test.pending:hover h2::after { 67 | content: '(pending)'; 68 | font-family: arial, sans-serif; 69 | } 70 | 71 | #mocha .test.pass.medium .duration { 72 | background: #c09853; 73 | } 74 | 75 | #mocha .test.pass.slow .duration { 76 | background: #b94a48; 77 | } 78 | 79 | #mocha .test.pass::before { 80 | content: '✓'; 81 | font-size: 12px; 82 | display: block; 83 | float: left; 84 | margin-right: 5px; 85 | color: #00d6b2; 86 | } 87 | 88 | #mocha .test.pass .duration { 89 | font-size: 9px; 90 | margin-left: 5px; 91 | padding: 2px 5px; 92 | color: #fff; 93 | -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.2); 94 | -moz-box-shadow: inset 0 1px 1px rgba(0,0,0,.2); 95 | box-shadow: inset 0 1px 1px rgba(0,0,0,.2); 96 | -webkit-border-radius: 5px; 97 | -moz-border-radius: 5px; 98 | -ms-border-radius: 5px; 99 | -o-border-radius: 5px; 100 | border-radius: 5px; 101 | } 102 | 103 | #mocha .test.pass.fast .duration { 104 | display: none; 105 | } 106 | 107 | #mocha .test.pending { 108 | color: #0b97c4; 109 | } 110 | 111 | #mocha .test.pending::before { 112 | content: '◦'; 113 | color: #0b97c4; 114 | } 115 | 116 | #mocha .test.fail { 117 | color: #c00; 118 | } 119 | 120 | #mocha .test.fail pre { 121 | color: black; 122 | } 123 | 124 | #mocha .test.fail::before { 125 | content: '✖'; 126 | font-size: 12px; 127 | display: block; 128 | float: left; 129 | margin-right: 5px; 130 | color: #c00; 131 | } 132 | 133 | #mocha .test pre.error { 134 | color: #c00; 135 | max-height: 300px; 136 | overflow: auto; 137 | } 138 | 139 | /** 140 | * (1): approximate for browsers not supporting calc 141 | * (2): 42 = 2*15 + 2*10 + 2*1 (padding + margin + border) 142 | * ^^ seriously 143 | */ 144 | #mocha .test pre { 145 | display: block; 146 | float: left; 147 | clear: left; 148 | font: 12px/1.5 monaco, monospace; 149 | margin: 5px; 150 | padding: 15px; 151 | border: 1px solid #eee; 152 | max-width: 85%; /*(1)*/ 153 | max-width: calc(100% - 42px); /*(2)*/ 154 | word-wrap: break-word; 155 | border-bottom-color: #ddd; 156 | -webkit-border-radius: 3px; 157 | -webkit-box-shadow: 0 1px 3px #eee; 158 | -moz-border-radius: 3px; 159 | -moz-box-shadow: 0 1px 3px #eee; 160 | border-radius: 3px; 161 | } 162 | 163 | #mocha .test h2 { 164 | position: relative; 165 | } 166 | 167 | #mocha .test a.replay { 168 | position: absolute; 169 | top: 3px; 170 | right: 0; 171 | text-decoration: none; 172 | vertical-align: middle; 173 | display: block; 174 | width: 15px; 175 | height: 15px; 176 | line-height: 15px; 177 | text-align: center; 178 | background: #eee; 179 | font-size: 15px; 180 | -moz-border-radius: 15px; 181 | border-radius: 15px; 182 | -webkit-transition: opacity 200ms; 183 | -moz-transition: opacity 200ms; 184 | transition: opacity 200ms; 185 | opacity: 0.3; 186 | color: #888; 187 | } 188 | 189 | #mocha .test:hover a.replay { 190 | opacity: 1; 191 | } 192 | 193 | #mocha-report.pass .test.fail { 194 | display: none; 195 | } 196 | 197 | #mocha-report.fail .test.pass { 198 | display: none; 199 | } 200 | 201 | #mocha-report.pending .test.pass, 202 | #mocha-report.pending .test.fail { 203 | display: none; 204 | } 205 | #mocha-report.pending .test.pass.pending { 206 | display: block; 207 | } 208 | 209 | #mocha-error { 210 | color: #c00; 211 | font-size: 1.5em; 212 | font-weight: 100; 213 | letter-spacing: 1px; 214 | } 215 | 216 | #mocha-stats { 217 | position: fixed; 218 | top: 15px; 219 | right: 10px; 220 | font-size: 12px; 221 | margin: 0; 222 | color: #888; 223 | z-index: 1; 224 | } 225 | 226 | #mocha-stats .progress { 227 | float: right; 228 | padding-top: 0; 229 | } 230 | 231 | #mocha-stats em { 232 | color: black; 233 | } 234 | 235 | #mocha-stats a { 236 | text-decoration: none; 237 | color: inherit; 238 | } 239 | 240 | #mocha-stats a:hover { 241 | border-bottom: 1px solid #eee; 242 | } 243 | 244 | #mocha-stats li { 245 | display: inline-block; 246 | margin: 0 5px; 247 | list-style: none; 248 | padding-top: 11px; 249 | } 250 | 251 | #mocha-stats canvas { 252 | width: 40px; 253 | height: 40px; 254 | } 255 | 256 | #mocha code .comment { color: #ddd; } 257 | #mocha code .init { color: #2f6fad; } 258 | #mocha code .string { color: #5890ad; } 259 | #mocha code .keyword { color: #8a6343; } 260 | #mocha code .number { color: #2f6fad; } 261 | 262 | @media screen and (max-device-width: 480px) { 263 | #mocha { 264 | margin: 60px 0px; 265 | } 266 | 267 | #mocha #stats { 268 | position: absolute; 269 | } 270 | } 271 | -------------------------------------------------------------------------------- /test/resources/basic_list.html: -------------------------------------------------------------------------------- 1 | 15 | -------------------------------------------------------------------------------- /test/resources/recursive_issue.html: -------------------------------------------------------------------------------- 1 | 10 | -------------------------------------------------------------------------------- /test/resources/recursive_list.html: -------------------------------------------------------------------------------- 1 | 15 | -------------------------------------------------------------------------------- /test/resources/seachange.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 |
6 |

The aim of this Portal is to optimise collective learning around the Syria crisis in order to improve international emergency response. The Portal will provide a single platform that brings together a broad range of relevant information, data, discussion and analysis of interest to different stakeholders - including those involved in operations, learning and evaluation and humanitarian policy. The Portal content will be categorised according to specific needs and topics.

7 |
8 |
9 | 10 | 11 |
12 |

Action Research on Community Adaptation in Bangladesh (ARCAB) is a collaborative platform for learning-by-doing action-research on community-based adaptation (CBA) to climate change. CBA is a "community-led process based on communities’, priorities, needs, knowledge, and capacities, which should empower people to plan for and cope with the impacts of climate change" (Reid et al., 2010). CBA is driven by vulnerable communities or starts by identifying communities most vulnerable to climate change. The purpose of ARCAB is to build a knowledge base around CBA action and knowledge transfer to enhance the capacity of NGOs to support climate resilient communities.

13 |
14 |
15 | 16 | 17 |
18 |

The Active Learning Network for Accountability and Performance in Humanitarian Action (ALNAP) was established in 1997, following the multi-agency evaluation of the Rwanda genocide. ALNAP is a collective response by the humanitarian sector, dedicated to improving humanitarian performance through increased learning and accountability.
19 | A unique network, ALNAP incorporates many of the key humanitarian organisations and experts from across the humanitarian sector. Members are drawn from donors, NGOs, the Red Cross/Crescent, the UN, independents and academics. ALNAP uses the broad range of experience and expertise within its membership to produce tools and analysis relevant and accessible to the humanitarian sector as a whole. ALNAP's workplan is aimed at improving humanitarian performance through learning and accountability, and consists of core activities, project activities and linked activities.

20 |
21 | 22 |
-------------------------------------------------------------------------------- /test/resources/table.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 |
firstnamelastnamepoints
JillSmith50
EveJackson94
JohnDoe80
AdamJohnson67
28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 |
firstnamelastnamepoints
JillSmith50
EveJackson94
JohnDoe80
AdamJohnson67
56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 |
FirstnameLastnamePoints
JillSmith50
EveJackson94
JohnDoe80
AdamJohnson67
84 | -------------------------------------------------------------------------------- /test/suites/ajaxSpiders.test.js: -------------------------------------------------------------------------------- 1 | ;(function(undefined) { 2 | 3 | /** 4 | * artoo spiders unit tests 5 | * ========================= 6 | * 7 | */ 8 | 9 | describe('artoo.ajaxSpider', function() { 10 | 11 | // Mock ajax responses 12 | var responses = { 13 | basic: { 14 | hello: 'world' 15 | }, 16 | basicWithBreak: { 17 | hello: 'world', 18 | breaking: true 19 | }, 20 | html: '
content1
content2
' 21 | }; 22 | 23 | var mocks = [ 24 | { 25 | url: '/basictxt/*', 26 | responseText: JSON.stringify(responses.basic) 27 | }, 28 | { 29 | url: '/basic/*', 30 | dataType: 'json', 31 | responseText: responses.basic 32 | }, 33 | { 34 | url: '/basic/4', 35 | dataType: 'json', 36 | responseText: responses.basicWithBreak 37 | }, 38 | { 39 | url: '/basictxt/4', 40 | responseText: JSON.stringify(responses.basicWithBreak) 41 | }, 42 | { 43 | url: '/html', 44 | responseText: responses.html 45 | }, 46 | { 47 | url: '/xml', 48 | dataType: 'xml', 49 | responseXML: responses.html 50 | } 51 | ]; 52 | 53 | // Registering mocks 54 | mocks.forEach(function(m) { 55 | $.mockjax(m); 56 | }); 57 | 58 | it('should be possible to launch a basic spiders.', function(done) { 59 | artoo.ajaxSpider(['/basictxt/1', '/basictxt/2'], function(data) { 60 | 61 | assert.deepEqual( 62 | data.map(JSON.parse), 63 | [responses.basic, responses.basic] 64 | ); 65 | done(); 66 | }); 67 | }); 68 | 69 | it('should not crash when crawling an empty list.', function(done) { 70 | 71 | artoo.ajaxSpider([], function(data) { 72 | assert.deepEqual( 73 | data, 74 | [] 75 | ); 76 | }); 77 | done(); 78 | }); 79 | 80 | it('should be possible to set limits to spiders.', function(done) { 81 | 82 | artoo.ajaxSpider( 83 | function(i, data) { 84 | if (i === 0) 85 | assert(data === undefined, 'First time iterator is run, data is undefined'); 86 | else 87 | assert.deepEqual( 88 | data, 89 | responses.basic, 90 | 'Next times iterator is run, data is correctly set.' 91 | ); 92 | return '/basic/' + i; 93 | }, 94 | { 95 | limit: 2, 96 | method: 'getJSON', 97 | done: function(data) { 98 | 99 | assert.deepEqual( 100 | data, 101 | [responses.basic, responses.basic], 102 | 'Crawling a with an iterator and a limit should stop properly.' 103 | ); 104 | done(); 105 | } 106 | } 107 | ); 108 | }); 109 | 110 | it('should provide some scraping helpers.', function(done) { 111 | 112 | async.parallel({ 113 | simpleScraper: function(next) { 114 | artoo.ajaxSpider( 115 | ['/html', '/html'], 116 | { 117 | scrape: { 118 | iterator: 'div', 119 | data: 'text' 120 | }, 121 | done: function(data) { 122 | assert.deepEqual( 123 | data, 124 | [['content1', 'content2'], ['content1', 'content2']], 125 | 'Crawling with a scraper should return the correct array.' 126 | ); 127 | next(); 128 | } 129 | } 130 | ); 131 | }, 132 | xmlScraper: function(next) { 133 | artoo.ajaxSpider( 134 | ['/xml', '/xml'], 135 | { 136 | scrape: { 137 | iterator: 'div', 138 | data: 'text' 139 | }, 140 | concat: true, 141 | settings: { 142 | dataType: 'xml', 143 | type: 'post' 144 | }, 145 | done: function(data) { 146 | 147 | assert.deepEqual( 148 | data, 149 | ['content1', 'content2', 'content1', 'content2'], 150 | 'Crawling with a concat scraper should return the correct array, even when data is XML document.' 151 | ); 152 | next(); 153 | } 154 | } 155 | ); 156 | }, 157 | jquerify: function(next) { 158 | artoo.ajaxSpider( 159 | function(i, $data) { 160 | if (i) 161 | assert(artoo.helpers.isSelector($data), 'Data given to iterator with jquerify is a valid selector.'); 162 | return '/html'; 163 | }, 164 | { 165 | limit: 2, 166 | jquerify: true, 167 | process: function($data, i)  { 168 | if (i) 169 | assert(artoo.helpers.isSelector($data), 'Data given to callbacks with jquerify is a valid selector.'); 170 | return artoo.scrape($data.find('div'), 'id'); 171 | }, 172 | done: function(data) { 173 | 174 | assert.deepEqual( 175 | data, 176 | [['div1', 'div2'], ['div1', 'div2']], 177 | 'Crawling with a jquerify spider returns the correct array.' 178 | ); 179 | next(); 180 | } 181 | } 182 | ); 183 | } 184 | }, done); 185 | }); 186 | }); 187 | }).call(this); 188 | -------------------------------------------------------------------------------- /test/suites/helpers.test.js: -------------------------------------------------------------------------------- 1 | ;(function(undefined) { 2 | 3 | /** 4 | * artoo helpers unit tests 5 | * ========================= 6 | * 7 | */ 8 | describe('artoo.helpers', function() { 9 | 10 | describe('first', function() { 11 | 12 | it('should return the correct item in the array.', function() { 13 | assert.strictEqual( 14 | artoo.helpers.first([1, 2, 3], function(e) { 15 | return e === 2; 16 | }), 17 | 2 18 | ); 19 | }); 20 | 21 | it('should return undefined if the item were not to be found.', function() { 22 | assert.strictEqual( 23 | artoo.helpers.first([1, 2, 3], function(e) { 24 | return e === 4; 25 | }), 26 | undefined 27 | ); 28 | }); 29 | }); 30 | 31 | describe('indexOf', function() { 32 | var a = [ 33 | {name: 'John'}, 34 | {name: 'Patrick'} 35 | ]; 36 | 37 | it('should return the correct index if the item exists.', function() { 38 | assert.strictEqual( 39 | artoo.helpers.indexOf(a, function(i) { 40 | return i.name === 'Patrick'; 41 | }), 42 | 1 43 | ); 44 | }); 45 | 46 | it('should return -1 if the item were not to be found.', function() { 47 | assert.strictEqual( 48 | artoo.helpers.indexOf(a, function(i) { 49 | return i.name === 'Jack'; 50 | }), 51 | -1 52 | ); 53 | }); 54 | }); 55 | 56 | describe('before', function() { 57 | it('should run the given function before the original one.', function() { 58 | var count = 0; 59 | 60 | var targetFunction = function() { 61 | count++; 62 | return 'ok'; 63 | }; 64 | 65 | // Monkey patching 66 | var newFunction = artoo.helpers.before(targetFunction, function() { 67 | count++; 68 | }); 69 | 70 | assert.strictEqual(newFunction(), 'ok'); 71 | assert.strictEqual(count, 2); 72 | }); 73 | }); 74 | }); 75 | }).call(this); 76 | -------------------------------------------------------------------------------- /test/suites/node.scrape.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * artoo scrape method node unit tests 3 | * =================================== 4 | * 5 | */ 6 | var cheerio = require('cheerio'), 7 | artoo = require('../../build/artoo.node.js'); 8 | 9 | function readFile(path) { 10 | return fs.readFileSync(path, 'utf-8'); 11 | } 12 | 13 | // Monkey patch - begin 14 | var assert = require('assert') 15 | var fs = require('fs') 16 | var $ 17 | 18 | helpers = { 19 | // Define a nodejs compliant fetchHTMLResource 20 | fetchHTMLResource: function (name, cb) { 21 | var $newDom = cheerio.load('
'); 22 | artoo.setContext($newDom) 23 | $newDom('#' + name).append(readFile(__dirname + '/../resources/' + name + '.html')); 24 | $ = $newDom 25 | cb('#' + name); 26 | } 27 | }; 28 | // Monkey patch - end 29 | 30 | eval(readFile(__dirname + '/scrape.test.js')) 31 | -------------------------------------------------------------------------------- /test/suites/node.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * artoo node unit tests 3 | * ====================== 4 | * 5 | * Testing artoo node.js integration. 6 | */ 7 | var assert = require('assert'), 8 | cheerio = require('cheerio'), 9 | artoo = require('../../build/artoo.node.js'); 10 | 11 | describe('artoo.node', function() { 12 | 13 | it('should override correctly isSelector.', function() { 14 | var $ = cheerio.load('
Hello
'); 15 | 16 | assert(artoo.helpers.isSelector($)); 17 | assert(artoo.helpers.isSelector($('div'))); 18 | }); 19 | 20 | it('should be possible to retrieve paths.', function() { 21 | assert.deepEqual(Object.keys(artoo.paths), ['browser', 'chrome', 'phantom']); 22 | }); 23 | 24 | it('should be possible to bootstrap a cheerio instance.', function() { 25 | var $ = cheerio.load(''); 26 | 27 | artoo.bootstrap(cheerio); 28 | 29 | assert.deepEqual($('li').scrape(), ['item1', 'item2']); 30 | }); 31 | 32 | it('should be possible to set artoo\'s internal context.', function() { 33 | var $ = cheerio.load(''); 34 | 35 | artoo.setContext($); 36 | 37 | assert.deepEqual(artoo.scrape('li'), ['item1', 'item2']); 38 | }); 39 | 40 | it('should be possible to scrape recursively.', function() { 41 | var $ = cheerio.load([ 42 | ''].join('\n')); 50 | 51 | artoo.setContext($); 52 | 53 | assert.deepEqual(artoo.scrape('ul.list > li', { 54 | scrape: { 55 | iterator: 'ul.sublist > li', 56 | data: 'text' 57 | } 58 | }), [['item1-1', 'item1-2'], ['item2-1', 'item2-2']]); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /test/suites/parsers.test.js: -------------------------------------------------------------------------------- 1 | ;(function(undefined) { 2 | 3 | /** 4 | * artoo parsers unit tests 5 | * ========================= 6 | * 7 | */ 8 | 9 | describe('artoo.parsers', function() { 10 | 11 | describe('url', function() { 12 | 13 | it('should be able to parse simple urls.', function() { 14 | 15 | assert.deepEqual( 16 | artoo.parsers.url('http://mydomain.com/'), 17 | { 18 | href: 'http://mydomain.com/', 19 | protocol: 'http', 20 | host: 'mydomain.com', 21 | hostname: 'mydomain.com', 22 | domain: 'mydomain', 23 | pathname: '/', 24 | path: '/', 25 | tld: 'com' 26 | } 27 | ); 28 | 29 | assert.deepEqual( 30 | artoo.parsers.url('http://mydomain.com/test'), 31 | { 32 | href: 'http://mydomain.com/test', 33 | protocol: 'http', 34 | host: 'mydomain.com', 35 | hostname: 'mydomain.com', 36 | domain: 'mydomain', 37 | pathname: '/test', 38 | path: '/test', 39 | tld: 'com' 40 | } 41 | ); 42 | 43 | assert.deepEqual( 44 | artoo.parsers.url('http://mydomain.com:8000/'), 45 | { 46 | href: 'http://mydomain.com:8000/', 47 | protocol: 'http', 48 | host: 'mydomain.com:8000', 49 | hostname: 'mydomain.com', 50 | domain: 'mydomain', 51 | pathname: '/', 52 | path: '/', 53 | port: 8000, 54 | tld: 'com' 55 | } 56 | ); 57 | 58 | assert.deepEqual( 59 | artoo.parsers.url('mydomain.com/'), 60 | { 61 | host: 'mydomain.com', 62 | hostname: 'mydomain.com', 63 | domain: 'mydomain', 64 | href: 'mydomain.com/', 65 | pathname: '/', 66 | path: '/', 67 | tld: 'com' 68 | } 69 | ); 70 | }); 71 | 72 | it('should be able to parse complex urls.', function() { 73 | assert.deepEqual( 74 | artoo.parsers.url('http://192.168.0.1:8000/'), 75 | { 76 | href: 'http://192.168.0.1:8000/', 77 | protocol: 'http', 78 | host: '192.168.0.1:8000', 79 | hostname: '192.168.0.1', 80 | domain: '192.168.0.1', 81 | pathname: '/', 82 | path: '/', 83 | port: 8000 84 | } 85 | ); 86 | 87 | assert.deepEqual( 88 | artoo.parsers.url('https://localhost:8000/example'), 89 | { 90 | href: 'https://localhost:8000/example', 91 | protocol: 'https', 92 | host: 'localhost:8000', 93 | hostname: 'localhost', 94 | domain: 'localhost', 95 | path: '/example', 96 | pathname: '/example', 97 | port: 8000 98 | } 99 | ); 100 | 101 | assert.deepEqual( 102 | artoo.parsers.url('http://sub.mydomain.com/'), 103 | { 104 | href: 'http://sub.mydomain.com/', 105 | protocol: 'http', 106 | host: 'sub.mydomain.com', 107 | hostname: 'sub.mydomain.com', 108 | domain: 'mydomain', 109 | subdomains: ['sub'], 110 | pathname: '/', 111 | path: '/', 112 | tld: 'com' 113 | } 114 | ); 115 | 116 | assert.deepEqual( 117 | artoo.parsers.url('http://sub2.sub1.mydomain.com/'), 118 | { 119 | href: 'http://sub2.sub1.mydomain.com/', 120 | protocol: 'http', 121 | host: 'sub2.sub1.mydomain.com', 122 | hostname: 'sub2.sub1.mydomain.com', 123 | domain: 'mydomain', 124 | subdomains: ['sub1', 'sub2'], 125 | pathname: '/', 126 | path: '/', 127 | tld: 'com' 128 | } 129 | ); 130 | 131 | assert.deepEqual( 132 | artoo.parsers.url('http://sub3.sub2.sub1.mydomain.com/'), 133 | { 134 | href: 'http://sub3.sub2.sub1.mydomain.com/', 135 | protocol: 'http', 136 | host: 'sub3.sub2.sub1.mydomain.com', 137 | hostname: 'sub3.sub2.sub1.mydomain.com', 138 | domain: 'mydomain', 139 | subdomains: ['sub1', 'sub2', 'sub3'], 140 | pathname: '/', 141 | path: '/', 142 | tld: 'com' 143 | } 144 | ); 145 | }); 146 | 147 | it('should handle querystrings.', function() { 148 | 149 | assert.deepEqual( 150 | artoo.parsers.url('https://mydomain.com/example?test¶m=yes'), 151 | { 152 | href: 'https://mydomain.com/example?test¶m=yes', 153 | protocol: 'https', 154 | host: 'mydomain.com', 155 | hostname: 'mydomain.com', 156 | domain: 'mydomain', 157 | pathname: '/example', 158 | path: '/example?test¶m=yes', 159 | search: '?test¶m=yes', 160 | query: { 161 | test: true, 162 | param: 'yes' 163 | }, 164 | tld: 'com' 165 | } 166 | ); 167 | }); 168 | 169 | it('should handle hash.', function() { 170 | 171 | assert.deepEqual( 172 | artoo.parsers.url('http://mydomain.com/example#table'), 173 | { 174 | href: 'http://mydomain.com/example#table', 175 | protocol: 'http', 176 | host: 'mydomain.com', 177 | hostname: 'mydomain.com', 178 | domain: 'mydomain', 179 | pathname: '/example', 180 | path: '/example#table', 181 | hash: '#table', 182 | tld: 'com' 183 | } 184 | ); 185 | }); 186 | 187 | it('should handle extensions.', function() { 188 | assert.deepEqual( 189 | artoo.parsers.url('http://mydomain.com/example.html'), 190 | { 191 | href: 'http://mydomain.com/example.html', 192 | protocol: 'http', 193 | host: 'mydomain.com', 194 | hostname: 'mydomain.com', 195 | domain: 'mydomain', 196 | pathname: '/example.html', 197 | path: '/example.html', 198 | tld: 'com', 199 | extension: 'html' 200 | } 201 | ); 202 | }); 203 | 204 | it('should handle authentification.', function() { 205 | assert.deepEqual( 206 | artoo.parsers.url('http://usr:pwd@mydomain.com/example.html'), 207 | { 208 | auth: { 209 | user: 'usr', 210 | password: 'pwd' 211 | }, 212 | href: 'http://usr:pwd@mydomain.com/example.html', 213 | protocol: 'http', 214 | host: 'mydomain.com', 215 | hostname: 'mydomain.com', 216 | domain: 'mydomain', 217 | pathname: '/example.html', 218 | path: '/example.html', 219 | tld: 'com', 220 | extension: 'html' 221 | } 222 | ); 223 | 224 | assert.deepEqual( 225 | artoo.parsers.url('http://usr@mydomain.com/example.html'), 226 | { 227 | auth: { 228 | user: 'usr' 229 | }, 230 | href: 'http://usr@mydomain.com/example.html', 231 | protocol: 'http', 232 | host: 'mydomain.com', 233 | hostname: 'mydomain.com', 234 | domain: 'mydomain', 235 | pathname: '/example.html', 236 | path: '/example.html', 237 | tld: 'com', 238 | extension: 'html' 239 | } 240 | ); 241 | }); 242 | }); 243 | }); 244 | }).call(this); 245 | -------------------------------------------------------------------------------- /test/suites/scrape.test.js: -------------------------------------------------------------------------------- 1 | ;(function(undefined) { 2 | 3 | /** 4 | * artoo scrape method unit tests 5 | * =============================== 6 | * 7 | */ 8 | 9 | describe('artoo.scrape', function() { 10 | 11 | it('basic list', function(done) { 12 | 13 | // Basic list scraping 14 | helpers.fetchHTMLResource('basic_list', function(id) { 15 | 16 | // Expected output 17 | var list = [ 18 | {url: 'http://nicesite.com', title: 'Nice site'}, 19 | {url: 'http://awesomesite.com', title: 'Awesome site'}, 20 | {url: 'http://prettysite.com', title: 'Pretty site'}, 21 | {url: 'http://unknownsite.com', title: 'Unknown site'} 22 | ]; 23 | 24 | var simpleList = [ 25 | 'http://nicesite.com', 26 | 'http://awesomesite.com', 27 | 'http://prettysite.com', 28 | 'http://unknownsite.com' 29 | ]; 30 | 31 | var titleList = [ 32 | 'Nice site', 33 | 'Awesome site', 34 | 'Pretty site', 35 | 'Unknown site' 36 | ]; 37 | 38 | // Testing 39 | assert.deepEqual( 40 | artoo.scrape(id + ' li > a', { 41 | url: {attr: 'href'}, 42 | title: {method: 'text'} 43 | }), 44 | list, 45 | 'Scraping the basic list should return the correct array of objects.' 46 | ); 47 | 48 | assert.deepEqual( 49 | $(id + ' li > a').scrape({ 50 | url: {attr: 'href'}, 51 | title: {method: 'text'} 52 | }), 53 | list, 54 | 'Scraping the basic list with the jQuery plugin should return the correct array of objects.' 55 | ); 56 | 57 | assert.deepEqual( 58 | artoo.scrape( 59 | function($) { 60 | return $(id + ' li:first-of-type > a').add(id + ' li:last-of-type > a'); 61 | } 62 | ), 63 | [titleList[0], titleList[3]], 64 | 'Scraping the list with a function iterator should return the correct array of first and last elements.' 65 | ); 66 | 67 | assert.deepEqual( 68 | artoo.scrape(id + ' li > a', { 69 | url: 'href', 70 | title: 'text' 71 | }), 72 | list, 73 | 'Scraping the basic list should return the correct array of objects through polymorphism.' 74 | ); 75 | 76 | assert.deepEqual( 77 | artoo.scrape(id + ' li > a', { 78 | url: function() {return $(this).attr('href');}, 79 | title: function() {return $(this).text();} 80 | }), 81 | list, 82 | 'Scraping the basic list with functions should return the correct array of objects.' 83 | ); 84 | 85 | assert.deepEqual( 86 | artoo.scrape(id + ' li > a', {attr: 'href'}), 87 | simpleList, 88 | 'Scraping only one property should return a correct array.' 89 | ); 90 | 91 | assert.deepEqual( 92 | artoo.scrape(id + ' li > a', 'href'), 93 | simpleList, 94 | 'Scraping only one property should return a correct array through polymorphism.' 95 | ); 96 | 97 | assert.deepEqual( 98 | artoo.scrape(id + ' li > a'), 99 | titleList, 100 | 'Scraping only one property with only first argument should return text.' 101 | ); 102 | 103 | assert.deepEqual( 104 | artoo.scrape(id + ' li > a', function() { return $(this).attr('href'); }), 105 | simpleList, 106 | 'Scraping only one property with a function should return a correct array.' 107 | ); 108 | 109 | assert.deepEqual( 110 | artoo.scrape(id + ' li > a', function() { return $(this).attr('href'); }), 111 | simpleList, 112 | 'Scraping only one property with a function should return a correct array.' 113 | ); 114 | 115 | assert.deepEqual( 116 | artoo.scrape(id + ' li > a', function() { return $(this).attr('href'); }, {limit: 2}), 117 | simpleList.slice(0, 2), 118 | 'Scraping with a limit should return only the first elements of the array.' 119 | ); 120 | 121 | assert.deepEqual( 122 | artoo.scrapeOne(id + ' li > a', function() { return $(this).attr('href'); }), 123 | simpleList.slice(0, 1)[0], 124 | 'Scraping only one item should return the correct element.' 125 | ); 126 | 127 | assert.deepEqual( 128 | $(id + ' li > a').scrapeOne(function() { return $(this).attr('href'); }), 129 | simpleList.slice(0, 1)[0], 130 | 'Scraping only one item with the jQuery plugin should return the correct element.' 131 | ); 132 | 133 | done(); 134 | }); 135 | }); 136 | 137 | it('complex list', function(done) { 138 | 139 | // Complex list scraping 140 | helpers.fetchHTMLResource('seachange', function(id) { 141 | 142 | // Expected output 143 | var list = [ 144 | { 145 | title: 'Syria Evaluation Portal for Coordinated Accountability and Lessons Learning (CALL)', 146 | url: 'http://www.syrialearning.org/' 147 | }, 148 | { 149 | title: 'Action Research on Community Adaptation in Bangladesh (ARCAB)', 150 | url: 'http://www.arcab.org/' 151 | }, 152 | { 153 | title: 'Active Learning Network for Accountability and Performance in humanitarian action (ALNAP)', 154 | url: 'http://www.alnap.org/' 155 | } 156 | ]; 157 | 158 | assert.deepEqual( 159 | artoo.scrape(id + ' .views-row', { 160 | url: { 161 | sel: 'a', 162 | attr: 'href' 163 | }, 164 | title: { 165 | sel: 'a', 166 | method: 'text' 167 | } 168 | }), 169 | list, 170 | 'Scraping the list should return the correct array.' 171 | ); 172 | 173 | assert.deepEqual( 174 | artoo.scrape({ 175 | iterator: '.views-row', 176 | data: { 177 | url: { 178 | sel: 'a', 179 | attr: 'href' 180 | }, 181 | title: { 182 | sel: 'a', 183 | method: 'text' 184 | } 185 | } 186 | }), 187 | list, 188 | 'Scraping the list with iterator polymorphism should also work.' 189 | ); 190 | 191 | assert.deepEqual( 192 | artoo.scrape(id + ' .views-row', { 193 | url: { 194 | sel: 'a', 195 | method: function() { 196 | return $(this).attr('href'); 197 | } 198 | }, 199 | title: { 200 | sel: 'a', 201 | method: function() { 202 | return $(this).text(); 203 | } 204 | } 205 | }), 206 | list, 207 | 'Scraping the list with method polymorphism should return the correct array.' 208 | ); 209 | 210 | assert.deepEqual( 211 | artoo.scrape(id + ' .views-row', { 212 | url: { 213 | sel: 'a', 214 | method: function($, el) { 215 | return $(el).attr('href'); 216 | } 217 | }, 218 | title: { 219 | sel: 'a', 220 | method: function($, el) { 221 | return $(el).text(); 222 | } 223 | } 224 | }), 225 | list, 226 | 'Scraping the list with methods el param should return the correct array.' 227 | ); 228 | 229 | assert.deepEqual( 230 | artoo.scrape(id + ' .views-row', { 231 | url: { 232 | sel: 'a', 233 | method: function() { 234 | return; 235 | }, 236 | defaultValue: 'default value' 237 | }, 238 | title: { 239 | sel: 'a', 240 | method: function() { 241 | return $(this).text(); 242 | } 243 | } 244 | }), 245 | list.map(function(i) { 246 | return { 247 | title: i.title, 248 | url: 'default value' 249 | }; 250 | }), 251 | 'Scraping the list with default values should return the correct array.' 252 | ); 253 | 254 | assert.deepEqual( 255 | artoo.scrapeOne(id + ' .views-row', { 256 | url: { 257 | sel: 'a', 258 | attr: 'href' 259 | }, 260 | title: { 261 | sel: 'a', 262 | method: 'text' 263 | } 264 | }), 265 | list.slice(0, 1)[0], 266 | 'Scraping one item should return the correct element.' 267 | ); 268 | 269 | assert.deepEqual( 270 | artoo.scrape(id + ' .views-row', { 271 | url: function() { 272 | return $(this).find('a').attr('href'); 273 | }, 274 | title: function() { 275 | return $(this).find('a').text(); 276 | } 277 | }), 278 | list, 279 | 'Scraping the list with functions should return the correct array.' 280 | ); 281 | 282 | done(); 283 | }); 284 | }); 285 | 286 | it('recursive list', function(done) { 287 | 288 | helpers.fetchHTMLResource('recursive_list', function(id) { 289 | 290 | var result1 = [ 291 | ['Item 1-1', 'Item 1-2'], 292 | ['Item 2-1', 'Item 2-2'] 293 | ]; 294 | 295 | var result2 = [ 296 | { 297 | name: 'List 1', 298 | items: [ 299 | { 300 | name: 'Foo', 301 | text: 'Item 1-1' 302 | }, 303 | { 304 | name: 'Bar', 305 | text: 'Item 1-2' 306 | } 307 | ] 308 | }, 309 | { 310 | name: 'List 2', 311 | items: [ 312 | { 313 | name: 'Oof', 314 | text: 'Item 2-1' 315 | }, 316 | { 317 | name: 'Rab', 318 | text: 'Item 2-2' 319 | } 320 | ] 321 | } 322 | ]; 323 | 324 | assert.deepEqual( 325 | artoo.scrape(id + ' .recursive-url-list1 > li', { 326 | scrape: { 327 | iterator: 'ul > li', 328 | data: 'text' 329 | } 330 | }), 331 | result1, 332 | 'Scraping the simple recursive list should return the correct array of arrays.' 333 | ); 334 | 335 | assert.deepEqual( 336 | artoo.scrape(id + ' .recursive-url-list2 > li', { 337 | name: 'name', 338 | items: { 339 | scrape: { 340 | iterator: 'ul > li', 341 | data: { 342 | name: 'name', 343 | text: 'text' 344 | } 345 | } 346 | } 347 | }), 348 | result2, 349 | 'Scraping the complex recursive list should return the correct items.' 350 | ); 351 | 352 | done(); 353 | }); 354 | }); 355 | 356 | it('recursive issue', function(done) { 357 | 358 | helpers.fetchHTMLResource('recursive_issue', function(id) { 359 | 360 | var result = { 361 | title: 'Title', 362 | items: ['Label 1', 'Label 2'] 363 | }; 364 | 365 | assert.deepEqual( 366 | artoo.scrapeOne(id + ' > ul', { 367 | title: { 368 | sel: 'li:first-of-type > span' 369 | }, 370 | items: { 371 | sel: 'li:nth-child(2)', 372 | scrape: { 373 | iterator: 'span', 374 | data: 'text' 375 | } 376 | } 377 | }), 378 | result, 379 | 'Scraping recursively should anchor on the correct selector.' 380 | ); 381 | 382 | done(); 383 | }); 384 | }); 385 | 386 | it('table', function(done) { 387 | 388 | helpers.fetchHTMLResource('table', function(id) { 389 | 390 | var flat = [ 391 | ['Jill', 'Smith', '50'], 392 | ['Eve', 'Jackson', '94'], 393 | ['John', 'Doe', '80'], 394 | ['Adam', 'Johnson', '67'] 395 | ]; 396 | 397 | var objects = flat.map(function(p) { 398 | return {firstname: p[0], lastname: p[1], points: p[2]}; 399 | }); 400 | 401 | var customs = flat.map(function(p) { 402 | return {foo: p[0], bar: p[1], baz: p[2]}; 403 | }); 404 | 405 | assert.deepEqual( 406 | artoo.scrape(id + ' .reference tr:not(:first-of-type)', { 407 | scrape: { 408 | iterator: 'td', 409 | data: 'text' 410 | } 411 | }), 412 | flat, 413 | 'Recursively scraping the table should return the correct array.' 414 | ); 415 | 416 | assert.deepEqual( 417 | artoo.scrapeTable(id + ' .reference'), 418 | flat, 419 | 'scrapTable should produce the same result as above.' 420 | ); 421 | 422 | assert.deepEqual( 423 | $(id + ' .reference').scrapeTable(), 424 | flat, 425 | 'Scraping a table with the jQuery plugin should work.' 426 | ); 427 | 428 | assert.deepEqual( 429 | artoo.scrape(id + ' .reference tr:not(:first-of-type)', { 430 | firstname: {sel: 'td:first-of-type'}, 431 | lastname: {sel: 'td:nth-of-type(2)'}, 432 | points: {sel: 'td:nth-of-type(3)'} 433 | }), 434 | objects, 435 | 'Scraping the list more easily should return the correct array of objects' 436 | ); 437 | 438 | assert.deepEqual( 439 | artoo.scrapeTable(id + ' .reference', {headers: 'th'}), 440 | objects, 441 | 'scrapTable with headers should produce the same result as above.' 442 | ); 443 | 444 | assert.deepEqual( 445 | artoo.scrapeTable(id + ' .reference-no-headers', {headers: 'first'}), 446 | objects, 447 | 'scrapTable with headers-first should produce the same result as above.' 448 | ); 449 | 450 | assert.deepEqual( 451 | artoo.scrapeTable(id + ' .reference', {headers: ['foo', 'bar', 'baz']}), 452 | customs, 453 | 'scrapTable with custom headers should produce the correct result.' 454 | ); 455 | 456 | assert.deepEqual( 457 | artoo.scrapeTable( 458 | id + ' .reference-no-headers', 459 | { 460 | headers: { 461 | type: 'first', 462 | method: function() { 463 | return $(this).text().toLowerCase(); 464 | } 465 | } 466 | }), 467 | objects, 468 | 'scrapTable with headers-first and header formatting should produce the same result as above.' 469 | ); 470 | 471 | done(); 472 | }); 473 | }); 474 | }); 475 | }).call(this); 476 | -------------------------------------------------------------------------------- /test/suites/store.test.js: -------------------------------------------------------------------------------- 1 | ;(function(undefined) { 2 | 3 | /** 4 | * artoo store methods unit tests 5 | * =============================== 6 | * 7 | */ 8 | 9 | describe('artoo.store', function() { 10 | 11 | // Cleaning the store and setting some value before we begin 12 | before(function() { 13 | artoo.s.removeAll(); 14 | 15 | artoo.s.set('number', 4); 16 | artoo.s.set('string', 'hello'); 17 | artoo.s.set('numberLikeString', '45'); 18 | artoo.s.set('array', [1, 2, 3]); 19 | artoo.s.set('object', {hello: 'world'}); 20 | }); 21 | 22 | // Deleting every localstorage item in case the tests failed 23 | after(function() { 24 | for (var i in localStorage) 25 | localStorage.removeItem(i); 26 | }); 27 | 28 | describe('synchronous stores', function() { 29 | 30 | it('should be possible to retrieve the store\'s keys.', function() { 31 | assert.deepEqual( 32 | artoo.s.keys().length, 33 | 5 34 | ); 35 | }); 36 | 37 | it('should be possible to get variable things from the store.', function() { 38 | assert.strictEqual( 39 | artoo.s.get('number'), 40 | 4, 41 | 'Getting a number from store should return the correct type.' 42 | ); 43 | 44 | assert.strictEqual( 45 | artoo.s.get('string'), 46 | 'hello', 47 | 'Getting a string from store should return the correct type.' 48 | ); 49 | 50 | assert.strictEqual( 51 | artoo.s.get('numberLikeString'), 52 | '45', 53 | 'Getting a string-like number from store should return the correct type.' 54 | ); 55 | 56 | assert.deepEqual( 57 | artoo.s.get('array'), 58 | [1, 2, 3], 59 | 'Getting an array from store should return the correct type.' 60 | ); 61 | 62 | assert.deepEqual( 63 | artoo.s.get('object'), 64 | {hello: 'world'}, 65 | 'Getting an object from store should return the correct type.' 66 | ); 67 | 68 | assert.deepEqual( 69 | artoo.s.getAll(), 70 | { 71 | array: [ 72 | 1, 73 | 2, 74 | 3 75 | ], 76 | number: 4, 77 | numberLikeString: '45', 78 | object: { 79 | hello: 'world' 80 | }, 81 | string: 'hello' 82 | }, 83 | 'Getting everything should return the correct object.' 84 | ); 85 | }); 86 | 87 | it('should provide some useful helpers when dealing with non-scalar values.', function() { 88 | assert.deepEqual( 89 | artoo.s.pushTo('array', 4), 90 | [1, 2, 3, 4], 91 | 'Pushing to a store array should return the correct new array.' 92 | ); 93 | 94 | assert.deepEqual( 95 | artoo.s.update('object', {yellow: 'blue'}), 96 | {hello: 'world', yellow: 'blue'}, 97 | 'Updating a store object should return the correct new object.' 98 | ); 99 | }); 100 | 101 | it('should provide some handy polymorphisms.', function() { 102 | assert.deepEqual( 103 | artoo.s(), 104 | artoo.s.getAll(), 105 | 's() and s.getAll achieve the same thing through polymorphism.' 106 | ); 107 | 108 | assert.deepEqual( 109 | artoo.s('object'), 110 | artoo.s.get('object'), 111 | 's(key) and s.get(key) achieve the same thing through polymorphism.' 112 | ); 113 | 114 | assert.deepEqual( 115 | artoo.s.get(), 116 | artoo.s.getAll(), 117 | 's.get without key and s.getAll achieve the same thing through polymorphism.' 118 | ); 119 | }); 120 | 121 | it('should be possible to remove items from the store.', function() { 122 | artoo.store.remove('number'); 123 | 124 | assert( 125 | artoo.s.keys().length === 4, 126 | 'The store should be one item less if we remove one.' 127 | ); 128 | 129 | assert.deepEqual( 130 | null, 131 | artoo.s.get('number'), 132 | 'Getting an inexistant item should return `null`.' 133 | ); 134 | 135 | artoo.s.removeAll(); 136 | 137 | assert( 138 | artoo.s.keys().length === 0, 139 | 'Removing every items from the store should leave an empty store.' 140 | ); 141 | }); 142 | }); 143 | 144 | describe('asynchronous stores', function() { 145 | var Mockup = new MockupAsynchronousStore(), 146 | asyncStore = artoo.createAsyncStore(Mockup.send); 147 | 148 | it('should be possible to set and get values.', function(done) { 149 | 150 | // Setting a value 151 | asyncStore.set('hello', 'world', function() { 152 | asyncStore.get('hello', function(data) { 153 | 154 | assert.strictEqual(data, 'world'); 155 | done(); 156 | }); 157 | }); 158 | }); 159 | 160 | it('should be possible to retrieve the whole store.', function(done) { 161 | asyncStore.set('color', 'blue') 162 | .then(function() { 163 | assert(true, 'Promise polymorphism should work.'); 164 | 165 | asyncStore.getAll() 166 | .then(function(data) { 167 | assert.deepEqual( 168 | data, 169 | {hello: 'world', color: 'blue'} 170 | ); 171 | 172 | done(); 173 | }); 174 | }); 175 | }); 176 | 177 | it('should be possible to retrieve store\'s keys and remove items.', function(done) { 178 | 179 | // Removing the 'color' key 180 | asyncStore.remove('color', function() { 181 | 182 | // Retrieving remaining keys to check whether deletion worked 183 | asyncStore.keys(function(keys) { 184 | assert.deepEqual( 185 | keys, 186 | ['hello'] 187 | ); 188 | }); 189 | 190 | done(); 191 | }); 192 | }); 193 | 194 | it('should be possible to completely clear the store.', function(done) { 195 | asyncStore.removeAll() 196 | .then(function() { 197 | 198 | // Retrieving store to assert 199 | asyncStore() 200 | .then(function(store) { 201 | assert(!Object.keys(store).length); 202 | done(); 203 | }); 204 | }); 205 | }); 206 | }); 207 | }); 208 | }).call(this); 209 | -------------------------------------------------------------------------------- /test/suites/writers.test.js: -------------------------------------------------------------------------------- 1 | ;(function(undefined) { 2 | 3 | /** 4 | * artoo writers unit tests 5 | * ========================= 6 | * 7 | */ 8 | describe('artoo.writers', function() { 9 | 10 | describe('csv', function() { 11 | var arrays = { 12 | correct: [['Michel', 'Chenu'], ['Marion', 'La brousse']], 13 | delimiter: [['Michel', 'Chenu, the Lord'], ['Marion', 'La brousse']], 14 | escape: [['Michel', 'Chenu'], ['Marion', 'dit "La brousse"']], 15 | badass: [['Michel', 'Chenu, the Lord'], ['Marion', 'dit "La brousse"']], 16 | linebreak: [ 17 | { a: 'toto', b: 'tata\n', c: 'titi' }, 18 | { a: 'tutu', b: 'pouet', c: 'blah' } 19 | ] 20 | }; 21 | 22 | var strings = { 23 | correct: 'Michel,Chenu\nMarion,La brousse', 24 | delimiter: 'Michel,"Chenu, the Lord"\nMarion,La brousse', 25 | escape: 'Michel,Chenu\nMarion,"dit ""La brousse"""', 26 | badass: 'Michel,"Chenu, the Lord"\nMarion,"dit ""La brousse"""', 27 | tsv: 'Michel\tChenu\nMarion\tLa brousse', 28 | customEscape: 'Michel,|Chenu, the Lord|\nMarion,La brousse', 29 | linebreak: 'a,b,c\ntoto,"tata\n",titi\ntutu,pouet,blah' 30 | }; 31 | 32 | var headerArray = [ 33 | { 34 | firstName: 'Michel', 35 | lastName: 'Chenu' 36 | }, 37 | { 38 | firstName: 'Marion', 39 | lastName: 'La brousse' 40 | } 41 | ]; 42 | 43 | var headerString = 'firstName,lastName\nMichel,Chenu\nMarion,La brousse', 44 | customString = 'one,two\nMichel,Chenu\nMarion,La brousse'; 45 | 46 | it('should be able to handle simple cases.', function() { 47 | for (var i in arrays) { 48 | assert.strictEqual( 49 | artoo.writers.csv(arrays[i]), 50 | strings[i] 51 | ); 52 | } 53 | }); 54 | 55 | it('should be able to handle custom delimiters.', function() { 56 | assert.strictEqual( 57 | artoo.writers.csv(arrays.correct, {delimiter: '\t'}), 58 | strings.tsv 59 | ); 60 | }); 61 | 62 | it('should be able to handle custom escape characters.', function() { 63 | assert.strictEqual( 64 | artoo.writers.csv(arrays.delimiter, {escape: '|'}), 65 | strings.customEscape 66 | ); 67 | }); 68 | 69 | it('should be able to output a string with correct headers.', function() { 70 | 71 | // Basic 72 | assert.strictEqual( 73 | artoo.writers.csv(headerArray), 74 | headerString 75 | ); 76 | 77 | // We don't want headers 78 | assert.strictEqual( 79 | artoo.writers.csv(headerArray, {headers: false}), 80 | strings.correct 81 | ); 82 | 83 | // Custom headers applied on array 84 | assert.strictEqual( 85 | artoo.writers.csv(arrays.correct, {headers: ['one', 'two']}), 86 | customString 87 | ); 88 | 89 | // Custom headers applied on array of objects 90 | assert.strictEqual( 91 | artoo.writers.csv(headerArray, {headers: ['one', 'two']}), 92 | customString 93 | ); 94 | }); 95 | 96 | it('should be able to output a CSV from other things than strings.', function() { 97 | assert.strictEqual( 98 | artoo.writers.csv([[1, 2], [3, 4]]), 99 | '1,2\n3,4' 100 | ); 101 | }); 102 | 103 | it('should be able to output a full CSV from array of items with different keys.', function() { 104 | assert.strictEqual( 105 | artoo.writers.csv([ 106 | { 107 | key1: 'ok', 108 | key2: 'ok' 109 | }, 110 | { 111 | key1: 'ko', 112 | key3: 'ko' 113 | } 114 | ]), 115 | 'key1,key2,key3\nok,ok,\nko,,ko' 116 | ); 117 | }); 118 | 119 | it('should be able to output a CSV with a specified order.', function() { 120 | assert.strictEqual( 121 | artoo.writers.csv([ 122 | { 123 | key1: 'ok', 124 | key2: 'ok' 125 | }, 126 | { 127 | key1: 'ko', 128 | key3: 'ko' 129 | } 130 | ], {order: ['key2', 'key1']}), 131 | 'key2,key1\nok,ok\n,ko' 132 | ); 133 | }); 134 | 135 | it('should be possible to combine an order and headers to output a CSV.', function() { 136 | assert.strictEqual( 137 | artoo.writers.csv([ 138 | { 139 | key1: 'ok', 140 | key2: 'ok' 141 | }, 142 | { 143 | key1: 'ko', 144 | key3: 'ko' 145 | } 146 | ], {order: ['key1', 'key2'], headers: ['Keyone', 'Keytwo']}), 147 | 'Keyone,Keytwo\nok,ok\nko,' 148 | ); 149 | }); 150 | }); 151 | }); 152 | }).call(this); 153 | -------------------------------------------------------------------------------- /test/unit.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Mocha Test Runner 8 | 9 | 14 | 15 | 16 |
17 |
18 | 19 | 20 | 21 | 22 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 117 | 118 | 119 | 120 | --------------------------------------------------------------------------------