Repository: mnmldave/scraper Branch: master Commit: cb75851385c3 Files: 45 Total size: 1.1 MB Directory structure: gitextract_812n7y4k/ ├── .gitignore ├── LICENSE.txt ├── README.md ├── Rakefile ├── psd/ │ ├── scraper128.psd │ ├── scraper32.psd │ └── scraper48.psd └── src/ ├── background.html ├── chrome_ex_oauth.html ├── chrome_ex_oauth.js ├── chrome_ex_oauthsimple.js ├── css/ │ ├── base.css │ ├── popup.css │ └── viewer.css ├── js/ │ ├── background.js │ ├── bit155/ │ │ ├── attr.js │ │ ├── csv.js │ │ └── scraper.js │ ├── contentscript.js │ ├── popup.js │ ├── shared.js │ └── viewer.js ├── lib/ │ ├── datatables-1.7.4/ │ │ ├── images/ │ │ │ └── Sorting icons.psd │ │ └── js/ │ │ └── jquery.dataTables.js │ ├── jquery-ui-1.8.6/ │ │ ├── css/ │ │ │ └── custom-theme/ │ │ │ └── jquery-ui-1.8.6.custom.css │ │ └── js/ │ │ ├── jquery-1.4.2.js │ │ ├── jquery-ui-1.8.6.highlight.js │ │ └── jquery-ui-1.8.6.js │ ├── jquery.layout-1.2.0.js │ └── jquery.tablednd_0_5.js ├── license.html ├── manifest.json ├── popup.html ├── test/ │ ├── SpecRunner.html │ ├── lib/ │ │ └── jasmine-1.0.1/ │ │ ├── MIT.LICENSE │ │ ├── jasmine-html.js │ │ ├── jasmine.css │ │ └── jasmine.js │ └── spec/ │ ├── bit155/ │ │ ├── attr.spec.js │ │ ├── csv.spec.js │ │ └── scraper.spec.js │ ├── jquery-commonAncestor.spec.js │ ├── jquery-serializeParams.spec.js │ └── jquery-xpath.spec.js └── viewer.html ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ syntax:glob build *.pem .DS_Store target Icon? ehthumbs.db Thumbs.db *.crx *.zip pkg ================================================ FILE: LICENSE.txt ================================================ Copyright (c) 2010, David Heaton All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of bit155 nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================ FILE: README.md ================================================ Scraper ======= A Google Chrome extension for getting data out of web pages and into spreadsheets. Usage ----- Highlight a part of the page that is similar to what you want to scrape. Right-click and select the "Scrape selected..." item. The scraper window will appear, showing you the initial results. You can export the table to by pressing the "Export to Google Docs..." button or use the left-hand pane to further refine or customize your scraping. The "Selector" section lets you change which page elements are scraped. You can specify the query as either a [jQuery selector](http://api.jquery.com/category/selectors/), or in [XPath](http://www.w3schools.com/XPath/xpath_intro.asp). You may also customize the columns of the table in the "Columns" section. These must be specified in XPath. You can specify names for columns if you would like. Selecting the "Exclude empty results" filter will prevent any matches that contain no column values from appearing in the table. After making any customizations, you must press the "Scrape" button to update the table of results. Download -------- Download the extension from [http://chrome.google.com/extensions/detail/mbigbapnjcgaffohmbkdlecaccepngjd](http://chrome.google.com/extensions/detail/mbigbapnjcgaffohmbkdlecaccepngjd). Get the sources from [https://github.com/mnmldave/scraper](https://github.com/mnmldave/scraper). Building -------- You don't need to 'build' this extension per se. To test it out, you first need to navigate to `chrome://extensions` from Google Chrome then expand "Developer Mode". Click the "Load unpacked extension..." button and point it to the `src` directory. Learn more about plugin development from the [Google Chrome Extensions](http://code.google.com/chrome/extensions/index.html "Google Chrome Extensions - Google Code") page. A `Rakefile` is included for compiling the Google Chrome extension into a zip file. It also does javascript and css minification. License ------- Scraper is open-sourced under a BSD license which you can find in `LICENSE.txt`. Credits ------- Many of the icons used in this extension are from the generous [Yusuke Kamiyamane](http://p.yusukekamiyamane.com/). ----------------------------------------------------------------------------- Copyright (c) 2010 David Heaton (dave@bit155.com) ================================================ FILE: Rakefile ================================================ # Copyright (c) 2010, David Heaton # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # * Neither the name of bit155 nor the names of its contributors # may be used to endorse or promote products derived from this software # without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. require 'json' require 'yui/compressor' require 'closure-compiler' task :default => [:rebuild, :repackage] # Metadata # -------------------------------------------------------------------------- manifest = open(File.join('src', 'manifest.json')) do |file| JSON.load(file) end name = manifest['name'] version = manifest['version'] # Building # -------------------------------------------------------------------------- build_dir = 'build' desc 'Rebuilds the extension' task :rebuild => [:clobber_build, :build] desc 'Removes build artifacts' task :clobber_build do rmtree build_dir rescue nil end desc 'Builds the extension' file build_dir do source_files = Dir.glob(File.join('src', '**')) mkdir_p build_dir rescue nil cp_r source_files, build_dir # compress css css_compressor = YUI::CssCompressor.new Dir.glob(File.join(build_dir, '**', '*.css')) do |path| puts 'Compressing: ' + path css = File.open(path, 'r') { |file| css_compressor.compress(file) } File.open(path, 'w') { |file| file.write(css) } end # compress javascript compiler = Closure::Compiler.new Dir.glob(File.join(build_dir, '**', '*.js')) do |path| puts 'Compiling: ' + path begin js = compiler.compile(File.read(path)) File.open(path, 'w') { |file| file.write(js) } rescue print 'Failed: ', $!, "\n" end end end # Packaging # -------------------------------------------------------------------------- package_name = "#{name}-#{version}" package_dir = 'pkg' package_dir_path = File.join(package_dir, package_name) zip_file = "#{package_name}.zip" # most of this packaging stuff right from rake/packagetask desc 'Packages the extension' task :package => ["#{package_dir}/#{zip_file}"] file "#{package_dir}/#{zip_file}" => package_dir_path do chdir(package_dir) do sh %{zip -r #{zip_file} #{package_name}} end end directory package_dir file package_dir_path => [package_dir, build_dir] do chdir(build_dir) do Dir.glob('**/*').each do |fn| f = File.join(File.dirname(__FILE__), package_dir_path, fn) fdir = File.dirname(f) mkdir_p(fdir) if !File.exist?(fdir) if File.directory?(fn) mkdir_p(f) else rm_f f safe_ln(fn, f) end end end end desc 'Removes the package artifacts' task :clobber_package do rmtree package_dir rescue nil end desc 'Repackages the extension' task :repackage => [:clobber_package, :package] desc 'Removes all rake artifacts' task :clobber => [:clobber_package, :clobber_build] ================================================ FILE: src/background.html ================================================ ================================================ FILE: src/chrome_ex_oauth.html ================================================ OAuth Redirect Page Redirecting... ================================================ FILE: src/chrome_ex_oauth.js ================================================ /** * Copyright (c) 2010 The Chromium Authors. All rights reserved. Use of this * source code is governed by a BSD-style license that can be found in the * LICENSE file. */ /** * Constructor - no need to invoke directly, call initBackgroundPage instead. * @constructor * @param {String} url_request_token The OAuth request token URL. * @param {String} url_auth_token The OAuth authorize token URL. * @param {String} url_access_token The OAuth access token URL. * @param {String} consumer_key The OAuth consumer key. * @param {String} consumer_secret The OAuth consumer secret. * @param {String} oauth_scope The OAuth scope parameter. * @param {Object} opt_args Optional arguments. Recognized parameters: * "app_name" {String} Name of the current application * "callback_page" {String} If you renamed chrome_ex_oauth.html, the name * this file was renamed to. */ function ChromeExOAuth(url_request_token, url_auth_token, url_access_token, consumer_key, consumer_secret, oauth_scope, opt_args) { this.url_request_token = url_request_token; this.url_auth_token = url_auth_token; this.url_access_token = url_access_token; this.consumer_key = consumer_key; this.consumer_secret = consumer_secret; this.oauth_scope = oauth_scope; this.app_name = opt_args && opt_args['app_name'] || "ChromeExOAuth Library"; this.key_token = "oauth_token"; this.key_token_secret = "oauth_token_secret"; this.callback_page = opt_args && opt_args['callback_page'] || "chrome_ex_oauth.html"; this.auth_params = {}; if (opt_args && opt_args['auth_params']) { for (key in opt_args['auth_params']) { if (opt_args['auth_params'].hasOwnProperty(key)) { this.auth_params[key] = opt_args['auth_params'][key]; } } } }; /******************************************************************************* * PUBLIC API METHODS * Call these from your background page. ******************************************************************************/ /** * Initializes the OAuth helper from the background page. You must call this * before attempting to make any OAuth calls. * @param {Object} oauth_config Configuration parameters in a JavaScript object. * The following parameters are recognized: * "request_url" {String} OAuth request token URL. * "authorize_url" {String} OAuth authorize token URL. * "access_url" {String} OAuth access token URL. * "consumer_key" {String} OAuth consumer key. * "consumer_secret" {String} OAuth consumer secret. * "scope" {String} OAuth access scope. * "app_name" {String} Application name. * "auth_params" {Object} Additional parameters to pass to the * Authorization token URL. For an example, 'hd', 'hl', 'btmpl': * http://code.google.com/apis/accounts/docs/OAuth_ref.html#GetAuth * @return {ChromeExOAuth} An initialized ChromeExOAuth object. */ ChromeExOAuth.initBackgroundPage = function(oauth_config) { window.chromeExOAuthConfig = oauth_config; window.chromeExOAuth = ChromeExOAuth.fromConfig(oauth_config); window.chromeExOAuthRedirectStarted = false; window.chromeExOAuthRequestingAccess = false; var url_match = chrome.extension.getURL(window.chromeExOAuth.callback_page); var tabs = {}; chrome.tabs.onUpdated.addListener(function(tabId, changeInfo, tab) { if (changeInfo.url && changeInfo.url.substr(0, url_match.length) === url_match && changeInfo.url != tabs[tabId] && window.chromeExOAuthRequestingAccess == false) { chrome.tabs.create({ 'url' : changeInfo.url }, function(tab) { tabs[tab.id] = tab.url; chrome.tabs.remove(tabId); }); } }); return window.chromeExOAuth; }; /** * Authorizes the current user with the configued API. You must call this * before calling sendSignedRequest. * @param {Function} callback A function to call once an access token has * been obtained. This callback will be passed the following arguments: * token {String} The OAuth access token. * secret {String} The OAuth access token secret. */ ChromeExOAuth.prototype.authorize = function(callback) { if (this.hasToken()) { callback(this.getToken(), this.getTokenSecret()); } else { window.chromeExOAuthOnAuthorize = function(token, secret) { callback(token, secret); }; chrome.tabs.create({ 'url' :chrome.extension.getURL(this.callback_page) }); } }; /** * Clears any OAuth tokens stored for this configuration. Effectively a * "logout" of the configured OAuth API. */ ChromeExOAuth.prototype.clearTokens = function() { delete localStorage[this.key_token + encodeURI(this.oauth_scope)]; delete localStorage[this.key_token_secret + encodeURI(this.oauth_scope)]; }; /** * Returns whether a token is currently stored for this configuration. * Effectively a check to see whether the current user is "logged in" to * the configured OAuth API. * @return {Boolean} True if an access token exists. */ ChromeExOAuth.prototype.hasToken = function() { return !!this.getToken(); }; /** * Makes an OAuth-signed HTTP request with the currently authorized tokens. * @param {String} url The URL to send the request to. Querystring parameters * should be omitted. * @param {Function} callback A function to be called once the request is * completed. This callback will be passed the following arguments: * responseText {String} The text response. * xhr {XMLHttpRequest} The XMLHttpRequest object which was used to * send the request. Useful if you need to check response status * code, etc. * @param {Object} opt_params Additional parameters to configure the request. * The following parameters are accepted: * "method" {String} The HTTP method to use. Defaults to "GET". * "body" {String} A request body to send. Defaults to null. * "parameters" {Object} Query parameters to include in the request. * "headers" {Object} Additional headers to include in the request. */ ChromeExOAuth.prototype.sendSignedRequest = function(url, callback, opt_params) { var method = opt_params && opt_params['method'] || 'GET'; var body = opt_params && opt_params['body'] || null; var params = opt_params && opt_params['parameters'] || {}; var headers = opt_params && opt_params['headers'] || {}; var signedUrl = this.signURL(url, method, params); ChromeExOAuth.sendRequest(method, signedUrl, headers, body, function (xhr) { if (xhr.readyState == 4) { callback(xhr.responseText, xhr); } }); }; /** * Adds the required OAuth parameters to the given url and returns the * result. Useful if you need a signed url but don't want to make an XHR * request. * @param {String} method The http method to use. * @param {String} url The base url of the resource you are querying. * @param {Object} opt_params Query parameters to include in the request. * @return {String} The base url plus any query params plus any OAuth params. */ ChromeExOAuth.prototype.signURL = function(url, method, opt_params) { var token = this.getToken(); var secret = this.getTokenSecret(); if (!token || !secret) { throw new Error("No oauth token or token secret"); } var params = opt_params || {}; var result = OAuthSimple().sign({ action : method, path : url, parameters : params, signatures: { consumer_key : this.consumer_key, shared_secret : this.consumer_secret, oauth_secret : secret, oauth_token: token } }); return result.signed_url; }; /** * Generates the Authorization header based on the oauth parameters. * @param {String} url The base url of the resource you are querying. * @param {Object} opt_params Query parameters to include in the request. * @return {String} An Authorization header containing the oauth_* params. */ ChromeExOAuth.prototype.getAuthorizationHeader = function(url, method, opt_params) { var token = this.getToken(); var secret = this.getTokenSecret(); if (!token || !secret) { throw new Error("No oauth token or token secret"); } var params = opt_params || {}; return OAuthSimple().getHeaderString({ action: method, path : url, parameters : params, signatures: { consumer_key : this.consumer_key, shared_secret : this.consumer_secret, oauth_secret : secret, oauth_token: token } }); }; /******************************************************************************* * PRIVATE API METHODS * Used by the library. There should be no need to call these methods directly. ******************************************************************************/ /** * Creates a new ChromeExOAuth object from the supplied configuration object. * @param {Object} oauth_config Configuration parameters in a JavaScript object. * The following parameters are recognized: * "request_url" {String} OAuth request token URL. * "authorize_url" {String} OAuth authorize token URL. * "access_url" {String} OAuth access token URL. * "consumer_key" {String} OAuth consumer key. * "consumer_secret" {String} OAuth consumer secret. * "scope" {String} OAuth access scope. * "app_name" {String} Application name. * "auth_params" {Object} Additional parameters to pass to the * Authorization token URL. For an example, 'hd', 'hl', 'btmpl': * http://code.google.com/apis/accounts/docs/OAuth_ref.html#GetAuth * @return {ChromeExOAuth} An initialized ChromeExOAuth object. */ ChromeExOAuth.fromConfig = function(oauth_config) { return new ChromeExOAuth( oauth_config['request_url'], oauth_config['authorize_url'], oauth_config['access_url'], oauth_config['consumer_key'], oauth_config['consumer_secret'], oauth_config['scope'], { 'app_name' : oauth_config['app_name'], 'auth_params' : oauth_config['auth_params'] } ); }; /** * Initializes chrome_ex_oauth.html and redirects the page if needed to start * the OAuth flow. Once an access token is obtained, this function closes * chrome_ex_oauth.html. */ ChromeExOAuth.initCallbackPage = function() { var background_page = chrome.extension.getBackgroundPage(); var oauth_config = background_page.chromeExOAuthConfig; var oauth = ChromeExOAuth.fromConfig(oauth_config); background_page.chromeExOAuthRedirectStarted = true; oauth.initOAuthFlow(function (token, secret) { background_page.chromeExOAuthOnAuthorize(token, secret); background_page.chromeExOAuthRedirectStarted = false; chrome.tabs.getSelected(null, function (tab) { chrome.tabs.remove(tab.id); }); }); }; /** * Sends an HTTP request. Convenience wrapper for XMLHttpRequest calls. * @param {String} method The HTTP method to use. * @param {String} url The URL to send the request to. * @param {Object} headers Optional request headers in key/value format. * @param {String} body Optional body content. * @param {Function} callback Function to call when the XMLHttpRequest's * ready state changes. See documentation for XMLHttpRequest's * onreadystatechange handler for more information. */ ChromeExOAuth.sendRequest = function(method, url, headers, body, callback) { var xhr = new XMLHttpRequest(); xhr.onreadystatechange = function(data) { callback(xhr, data); } xhr.open(method, url, true); if (headers) { for (var header in headers) { if (headers.hasOwnProperty(header)) { xhr.setRequestHeader(header, headers[header]); } } } xhr.send(body); }; /** * Decodes a URL-encoded string into key/value pairs. * @param {String} encoded An URL-encoded string. * @return {Object} An object representing the decoded key/value pairs found * in the encoded string. */ ChromeExOAuth.formDecode = function(encoded) { var params = encoded.split("&"); var decoded = {}; for (var i = 0, param; param = params[i]; i++) { var keyval = param.split("="); if (keyval.length == 2) { var key = ChromeExOAuth.fromRfc3986(keyval[0]); var val = ChromeExOAuth.fromRfc3986(keyval[1]); decoded[key] = val; } } return decoded; }; /** * Returns the current window's querystring decoded into key/value pairs. * @return {Object} A object representing any key/value pairs found in the * current window's querystring. */ ChromeExOAuth.getQueryStringParams = function() { var urlparts = window.location.href.split("?"); if (urlparts.length >= 2) { var querystring = urlparts.slice(1).join("?"); return ChromeExOAuth.formDecode(querystring); } return {}; }; /** * Binds a function call to a specific object. This function will also take * a variable number of additional arguments which will be prepended to the * arguments passed to the bound function when it is called. * @param {Function} func The function to bind. * @param {Object} obj The object to bind to the function's "this". * @return {Function} A closure that will call the bound function. */ ChromeExOAuth.bind = function(func, obj) { var newargs = Array.prototype.slice.call(arguments).slice(2); return function() { var combinedargs = newargs.concat(Array.prototype.slice.call(arguments)); func.apply(obj, combinedargs); }; }; /** * Encodes a value according to the RFC3986 specification. * @param {String} val The string to encode. */ ChromeExOAuth.toRfc3986 = function(val){ return encodeURIComponent(val) .replace(/\!/g, "%21") .replace(/\*/g, "%2A") .replace(/'/g, "%27") .replace(/\(/g, "%28") .replace(/\)/g, "%29"); }; /** * Decodes a string that has been encoded according to RFC3986. * @param {String} val The string to decode. */ ChromeExOAuth.fromRfc3986 = function(val){ var tmp = val .replace(/%21/g, "!") .replace(/%2A/g, "*") .replace(/%27/g, "'") .replace(/%28/g, "(") .replace(/%29/g, ")"); return decodeURIComponent(tmp); }; /** * Adds a key/value parameter to the supplied URL. * @param {String} url An URL which may or may not contain querystring values. * @param {String} key A key * @param {String} value A value * @return {String} The URL with URL-encoded versions of the key and value * appended, prefixing them with "&" or "?" as needed. */ ChromeExOAuth.addURLParam = function(url, key, value) { var sep = (url.indexOf('?') >= 0) ? "&" : "?"; return url + sep + ChromeExOAuth.toRfc3986(key) + "=" + ChromeExOAuth.toRfc3986(value); }; /** * Stores an OAuth token for the configured scope. * @param {String} token The token to store. */ ChromeExOAuth.prototype.setToken = function(token) { localStorage[this.key_token + encodeURI(this.oauth_scope)] = token; }; /** * Retrieves any stored token for the configured scope. * @return {String} The stored token. */ ChromeExOAuth.prototype.getToken = function() { return localStorage[this.key_token + encodeURI(this.oauth_scope)]; }; /** * Stores an OAuth token secret for the configured scope. * @param {String} secret The secret to store. */ ChromeExOAuth.prototype.setTokenSecret = function(secret) { localStorage[this.key_token_secret + encodeURI(this.oauth_scope)] = secret; }; /** * Retrieves any stored secret for the configured scope. * @return {String} The stored secret. */ ChromeExOAuth.prototype.getTokenSecret = function() { return localStorage[this.key_token_secret + encodeURI(this.oauth_scope)]; }; /** * Starts an OAuth authorization flow for the current page. If a token exists, * no redirect is needed and the supplied callback is called immediately. * If this method detects that a redirect has finished, it grabs the * appropriate OAuth parameters from the URL and attempts to retrieve an * access token. If no token exists and no redirect has happened, then * an access token is requested and the page is ultimately redirected. * @param {Function} callback The function to call once the flow has finished. * This callback will be passed the following arguments: * token {String} The OAuth access token. * secret {String} The OAuth access token secret. */ ChromeExOAuth.prototype.initOAuthFlow = function(callback) { if (!this.hasToken()) { var params = ChromeExOAuth.getQueryStringParams(); if (params['chromeexoauthcallback'] == 'true') { var oauth_token = params['oauth_token']; var oauth_verifier = params['oauth_verifier'] this.getAccessToken(oauth_token, oauth_verifier, callback); } else { var request_params = { 'url_callback_param' : 'chromeexoauthcallback' } this.getRequestToken(function(url) { window.location.href = url; }, request_params); } } else { callback(this.getToken(), this.getTokenSecret()); } }; /** * Requests an OAuth request token. * @param {Function} callback Function to call once the authorize URL is * calculated. This callback will be passed the following arguments: * url {String} The URL the user must be redirected to in order to * approve the token. * @param {Object} opt_args Optional arguments. The following parameters * are accepted: * "url_callback" {String} The URL the OAuth provider will redirect to. * "url_callback_param" {String} A parameter to include in the callback * URL in order to indicate to this library that a redirect has * taken place. */ ChromeExOAuth.prototype.getRequestToken = function(callback, opt_args) { if (typeof callback !== "function") { throw new Error("Specified callback must be a function."); } var url = opt_args && opt_args['url_callback'] || window && window.top && window.top.location && window.top.location.href; var url_param = opt_args && opt_args['url_callback_param'] || "chromeexoauthcallback"; var url_callback = ChromeExOAuth.addURLParam(url, url_param, "true"); var result = OAuthSimple().sign({ path : this.url_request_token, parameters: { "xoauth_displayname" : this.app_name, "scope" : this.oauth_scope, "oauth_callback" : url_callback }, signatures: { consumer_key : this.consumer_key, shared_secret : this.consumer_secret } }); var onToken = ChromeExOAuth.bind(this.onRequestToken, this, callback); ChromeExOAuth.sendRequest("GET", result.signed_url, null, null, onToken); }; /** * Called when a request token has been returned. Stores the request token * secret for later use and sends the authorization url to the supplied * callback (for redirecting the user). * @param {Function} callback Function to call once the authorize URL is * calculated. This callback will be passed the following arguments: * url {String} The URL the user must be redirected to in order to * approve the token. * @param {XMLHttpRequest} xhr The XMLHttpRequest object used to fetch the * request token. */ ChromeExOAuth.prototype.onRequestToken = function(callback, xhr) { if (xhr.readyState == 4) { if (xhr.status == 200) { var params = ChromeExOAuth.formDecode(xhr.responseText); var token = params['oauth_token']; this.setTokenSecret(params['oauth_token_secret']); var url = ChromeExOAuth.addURLParam(this.url_auth_token, "oauth_token", token); for (var key in this.auth_params) { if (this.auth_params.hasOwnProperty(key)) { url = ChromeExOAuth.addURLParam(url, key, this.auth_params[key]); } } callback(url); } else { throw new Error("Fetching request token failed. Status " + xhr.status); } } }; /** * Requests an OAuth access token. * @param {String} oauth_token The OAuth request token. * @param {String} oauth_verifier The OAuth token verifier. * @param {Function} callback The function to call once the token is obtained. * This callback will be passed the following arguments: * token {String} The OAuth access token. * secret {String} The OAuth access token secret. */ ChromeExOAuth.prototype.getAccessToken = function(oauth_token, oauth_verifier, callback) { if (typeof callback !== "function") { throw new Error("Specified callback must be a function."); } var bg = chrome.extension.getBackgroundPage(); if (bg.chromeExOAuthRequestingAccess == false) { bg.chromeExOAuthRequestingAccess = true; var result = OAuthSimple().sign({ path : this.url_access_token, parameters: { "oauth_token" : oauth_token, "oauth_verifier" : oauth_verifier }, signatures: { consumer_key : this.consumer_key, shared_secret : this.consumer_secret, oauth_secret : this.getTokenSecret(this.oauth_scope) } }); var onToken = ChromeExOAuth.bind(this.onAccessToken, this, callback); ChromeExOAuth.sendRequest("GET", result.signed_url, null, null, onToken); } }; /** * Called when an access token has been returned. Stores the access token and * access token secret for later use and sends them to the supplied callback. * @param {Function} callback The function to call once the token is obtained. * This callback will be passed the following arguments: * token {String} The OAuth access token. * secret {String} The OAuth access token secret. * @param {XMLHttpRequest} xhr The XMLHttpRequest object used to fetch the * access token. */ ChromeExOAuth.prototype.onAccessToken = function(callback, xhr) { if (xhr.readyState == 4) { var bg = chrome.extension.getBackgroundPage(); if (xhr.status == 200) { var params = ChromeExOAuth.formDecode(xhr.responseText); var token = params["oauth_token"]; var secret = params["oauth_token_secret"]; this.setToken(token); this.setTokenSecret(secret); bg.chromeExOAuthRequestingAccess = false; callback(token, secret); } else { bg.chromeExOAuthRequestingAccess = false; throw new Error("Fetching access token failed with status " + xhr.status); } } }; ================================================ FILE: src/chrome_ex_oauthsimple.js ================================================ /* OAuthSimple * A simpler version of OAuth * * author: jr conlin * mail: src@anticipatr.com * copyright: unitedHeroes.net * version: 1.0 * url: http://unitedHeroes.net/OAuthSimple * * Copyright (c) 2009, unitedHeroes.net * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of the unitedHeroes.net nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY UNITEDHEROES.NET ''AS IS'' AND ANY * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL UNITEDHEROES.NET BE LIABLE FOR ANY * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ var OAuthSimple; if (OAuthSimple === undefined) { /* Simple OAuth * * This class only builds the OAuth elements, it does not do the actual * transmission or reception of the tokens. It does not validate elements * of the token. It is for client use only. * * api_key is the API key, also known as the OAuth consumer key * shared_secret is the shared secret (duh). * * Both the api_key and shared_secret are generally provided by the site * offering OAuth services. You need to specify them at object creation * because nobody ing uses OAuth without that minimal set of * signatures. * * If you want to use the higher order security that comes from the * OAuth token (sorry, I don't provide the functions to fetch that because * sites aren't horribly consistent about how they offer that), you need to * pass those in either with .setTokensAndSecrets() or as an argument to the * .sign() or .getHeaderString() functions. * * Example: var oauthObject = OAuthSimple().sign({path:'http://example.com/rest/', parameters: 'foo=bar&gorp=banana', signatures:{ api_key:'12345abcd', shared_secret:'xyz-5309' }}); document.getElementById('someLink').href=oauthObject.signed_url; * * that will sign as a "GET" using "SHA1-MAC" the url. If you need more than * that, read on, McDuff. */ /** OAuthSimple creator * * Create an instance of OAuthSimple * * @param api_key {string} The API Key (sometimes referred to as the consumer key) This value is usually supplied by the site you wish to use. * @param shared_secret (string) The shared secret. This value is also usually provided by the site you wish to use. */ OAuthSimple = function (consumer_key,shared_secret) { /* if (api_key == undefined) throw("Missing argument: api_key (oauth_consumer_key) for OAuthSimple. This is usually provided by the hosting site."); if (shared_secret == undefined) throw("Missing argument: shared_secret (shared secret) for OAuthSimple. This is usually provided by the hosting site."); */ this._secrets={}; this._parameters={}; // General configuration options. if (consumer_key !== undefined) { this._secrets['consumer_key'] = consumer_key; } if (shared_secret !== undefined) { this._secrets['shared_secret'] = shared_secret; } this._default_signature_method= "HMAC-SHA1"; this._action = "GET"; this._nonce_chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; this.reset = function() { this._parameters={}; this._path=undefined; return this; }; /** set the parameters either from a hash or a string * * @param {string,object} List of parameters for the call, this can either be a URI string (e.g. "foo=bar&gorp=banana" or an object/hash) */ this.setParameters = function (parameters) { if (parameters === undefined) { parameters = {}; } if (typeof(parameters) == 'string') { parameters=this._parseParameterString(parameters); } this._parameters = parameters; if (this._parameters['oauth_nonce'] === undefined) { this._getNonce(); } if (this._parameters['oauth_timestamp'] === undefined) { this._getTimestamp(); } if (this._parameters['oauth_method'] === undefined) { this.setSignatureMethod(); } if (this._parameters['oauth_consumer_key'] === undefined) { this._getApiKey(); } if(this._parameters['oauth_token'] === undefined) { this._getAccessToken(); } return this; }; /** convienence method for setParameters * * @param parameters {string,object} See .setParameters */ this.setQueryString = function (parameters) { return this.setParameters(parameters); }; /** Set the target URL (does not include the parameters) * * @param path {string} the fully qualified URI (excluding query arguments) (e.g "http://example.org/foo") */ this.setURL = function (path) { if (path == '') { throw ('No path specified for OAuthSimple.setURL'); } this._path = path; return this; }; /** convienence method for setURL * * @param path {string} see .setURL */ this.setPath = function(path){ return this.setURL(path); }; /** set the "action" for the url, (e.g. GET,POST, DELETE, etc.) * * @param action {string} HTTP Action word. */ this.setAction = function(action) { if (action === undefined) { action="GET"; } action = action.toUpperCase(); if (action.match('[^A-Z]')) { throw ('Invalid action specified for OAuthSimple.setAction'); } this._action = action; return this; }; /** set the signatures (as well as validate the ones you have) * * @param signatures {object} object/hash of the token/signature pairs {api_key:, shared_secret:, oauth_token: oauth_secret:} */ this.setTokensAndSecrets = function(signatures) { if (signatures) { for (var i in signatures) { this._secrets[i] = signatures[i]; } } // Aliases if (this._secrets['api_key']) { this._secrets.consumer_key = this._secrets.api_key; } if (this._secrets['access_token']) { this._secrets.oauth_token = this._secrets.access_token; } if (this._secrets['access_secret']) { this._secrets.oauth_secret = this._secrets.access_secret; } // Gauntlet if (this._secrets.consumer_key === undefined) { throw('Missing required consumer_key in OAuthSimple.setTokensAndSecrets'); } if (this._secrets.shared_secret === undefined) { throw('Missing required shared_secret in OAuthSimple.setTokensAndSecrets'); } if ((this._secrets.oauth_token !== undefined) && (this._secrets.oauth_secret === undefined)) { throw('Missing oauth_secret for supplied oauth_token in OAuthSimple.setTokensAndSecrets'); } return this; }; /** set the signature method (currently only Plaintext or SHA-MAC1) * * @param method {string} Method of signing the transaction (only PLAINTEXT and SHA-MAC1 allowed for now) */ this.setSignatureMethod = function(method) { if (method === undefined) { method = this._default_signature_method; } //TODO: accept things other than PlainText or SHA-MAC1 if (method.toUpperCase().match(/(PLAINTEXT|HMAC-SHA1)/) === undefined) { throw ('Unknown signing method specified for OAuthSimple.setSignatureMethod'); } this._parameters['oauth_signature_method']= method.toUpperCase(); return this; }; /** sign the request * * note: all arguments are optional, provided you've set them using the * other helper functions. * * @param args {object} hash of arguments for the call * {action:, path:, parameters:, method:, signatures:} * all arguments are optional. */ this.sign = function (args) { if (args === undefined) { args = {}; } // Set any given parameters if(args['action'] !== undefined) { this.setAction(args['action']); } if (args['path'] !== undefined) { this.setPath(args['path']); } if (args['method'] !== undefined) { this.setSignatureMethod(args['method']); } this.setTokensAndSecrets(args['signatures']); if (args['parameters'] !== undefined){ this.setParameters(args['parameters']); } // check the parameters var normParams = this._normalizedParameters(); this._parameters['oauth_signature']=this._generateSignature(normParams); return { parameters: this._parameters, signature: this._oauthEscape(this._parameters['oauth_signature']), signed_url: this._path + '?' + this._normalizedParameters(), header: this.getHeaderString() }; }; /** Return a formatted "header" string * * NOTE: This doesn't set the "Authorization: " prefix, which is required. * I don't set it because various set header functions prefer different * ways to do that. * * @param args {object} see .sign */ this.getHeaderString = function(args) { if (this._parameters['oauth_signature'] === undefined) { this.sign(args); } var result = 'OAuth '; for (var pName in this._parameters) { if (!pName.match(/^oauth/)) { continue; } if ((this._parameters[pName]) instanceof Array) { var pLength = this._parameters[pName].length; for (var j=0;j>16)+(y>>16)+(l>>16);return(m<<16)|(l&0xFFFF);}function _r(n,c){return(n<>>(32-c));}function _c(x,l){x[l>>5]|=0x80<<(24-l%32);x[((l+64>>9)<<4)+15]=l;var w=[80],a=1732584193,b=-271733879,c=-1732584194,d=271733878,e=-1009589776;for(var i=0;i>5]|=(s.charCodeAt(i/8)&m)<<(32-_z-i%32);}return b;}function _h(k,d){var b=_b(k);if(b.length>16){b=_c(b,k.length*_z);}var p=[16],o=[16];for(var i=0;i<16;i++){p[i]=b[i]^0x36363636;o[i]=b[i]^0x5C5C5C5C;}var h=_c(p.concat(_b(d)),512+d.length*_z);return _c(o.concat(h),512+160);}function _n(b){var t="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",s='';for(var i=0;i>2]>>8*(3-i%4))&0xFF)<<16)|(((b[i+1>>2]>>8*(3-(i+1)%4))&0xFF)<<8)|((b[i+2>>2]>>8*(3-(i+2)%4))&0xFF);for(var j=0;j<4;j++){if(i*8+j*6>b.length*32){s+=_p;}else{s+=t.charAt((r>>6*(3-j))&0x3F);}}}return s;}function _x(k,d){return _n(_h(k,d));}return _x(k,d); } this._normalizedParameters = function() { var elements = new Array(); var paramNames = []; var ra =0; for (var paramName in this._parameters) { if (ra++ > 1000) { throw('runaway 1'); } paramNames.unshift(paramName); } paramNames = paramNames.sort(); pLen = paramNames.length; for (var i=0;i 1000) { throw('runaway 1'); } elements.push(this._oauthEscape(paramName) + '=' + this._oauthEscape(sorted[j])); } continue; } elements.push(this._oauthEscape(paramName) + '=' + this._oauthEscape(this._parameters[paramName])); } return elements.join('&'); }; this._generateSignature = function() { var secretKey = this._oauthEscape(this._secrets.shared_secret)+'&'+ this._oauthEscape(this._secrets.oauth_secret); if (this._parameters['oauth_signature_method'] == 'PLAINTEXT') { return secretKey; } if (this._parameters['oauth_signature_method'] == 'HMAC-SHA1') { var sigString = this._oauthEscape(this._action)+'&'+this._oauthEscape(this._path)+'&'+this._oauthEscape(this._normalizedParameters()); return this.b64_hmac_sha1(secretKey,sigString); } return null; }; return this; }; } ================================================ FILE: src/css/base.css ================================================ html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, font, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, b, u, i, center, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td { margin: 0; padding: 0; border: 0; outline: 0; font-size: 100%; vertical-align: baseline; background: transparent; } body { line-height: 1; font-family: "Lucida Grande",Arial,sans-serif; font-size: 10px; color: #333; } ol, ul { list-style: none; } blockquote, q { quotes: none; } blockquote:before, blockquote:after, q:before, q:after { content: ''; content: none; } :focus { outline: 0; } ins { text-decoration: none; } del { text-decoration: line-through; } table { border-collapse: collapse; border-spacing: 0; } p { margin-bottom: 1em; line-height: 1.5; } a { color: #4492D7; text-decoration: none; } a:hover { text-decoration: underline; } .disabled { color: #aaa; } a.button, button, input[type=submit] { outline: none; border: 1px solid #aaa; padding: 7px 10px; border-radius: 5px; background: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#ddd)); color: #333; text-shadow: 0 1px rgba(255,255,255,0.8); text-decoration: none; font-size: 11px; } a.button:active, button:active, input[type=submit]:active { background: -webkit-gradient(linear, left top, left bottom, from(#ddd), to(#eee)); } a.button:focus, button:focus, input[type=submit]:focus { border-color: #999; -webkit-box-shadow: 0 0 3px rgba(0,0,0,0.3); } select { border: 1px solid #aaa; padding: 3px; } ================================================ FILE: src/css/popup.css ================================================ @import url('base.css'); html { width: 300px; } body { margin: 10px; padding: 0; } #presets { margin: 0; overflow: auto; max-height: 300px; } #presets li { list-style: none; margin: 0; } #presets .preset a { color: #333; display: block; text-decoration: none; padding: 10px; padding-left: 31px; background-image: url('../img/application-form.png'); background-repeat: no-repeat; background-position: 10px; } #presets .preset a:focus { outline: none; } #presets .preset:hover { background-color: #eee; text-decoration: underline; } #footer { margin-top: 10px; padding-top: 10px; text-align: right; border-top: 1px solid #999; } #scraper { float: left; text-decoration: none; font-weight: bold; color: #333; } ================================================ FILE: src/css/viewer.css ================================================ @import url('base.css'); body { font-family: "Lucida Grande",Arial,sans-serif; font-size: 10px; margin: 0; padding: 0; } a:focus { outline: none; } div.error { line-height: 1.5; } div.error.ui-dialog-content.ui-widget-content { padding-left: 40px; background-repeat: no-repeat; background-position: 12px 12px; background-image: url('../img/exclamation-red.png'); } #presets { display: none; padding-top: 10px; } #presets-form { padding-bottom: 15px; } #presets-form fieldset { margin-top: 10px; padding-top: 5px; border: none; border-top: 1px solid #aaa; } #presets-form-name { width: 70%; border: 1px solid #999; border-radius: 5px; -webkit-box-shadow: inset 1px 2px 3px rgba(0,0,0,0.2); padding: 5px 5px; margin-bottom: 10px; } #presets-list { margin: 0; padding: 0; overflow: auto; } #presets-list li { list-style: none; margin: 0; padding: 10px; } #presets-list li a { color: #333; } #presets-list li a:focus { outline: none; } #presets-list li:hover { background-color: #eee; } #presets-list li { padding-right: 26px; } #presets-list li .preset-handle { cursor: move; display: block; float: left; } #presets-list li .preset-load { cursor: pointer; display: block; margin-top: 2px; margin-left: 26px; margin-right: 26px; line-height: 1.5; } #presets-list li .preset-remove { display: inline-block; position: absolute; right: 16px; } #options { -webkit-user-select: none; } #options-header { min-height: 30px; border-top: 1px solid #fff !important; background: -webkit-gradient(linear, left bottom, left top, color-stop(0.1, #ccc), color-stop(0.8, #eee)); padding: 10px; line-height: 1.2; } #options-meta-page { background-image: url('../img/scraper32.png'); background-position: top left; background-repeat: no-repeat; min-height: 32px; padding-left: 42px; } #options-meta-page a { text-decoration: none; color: #444; text-shadow: 0 1px 0 #fff; font-weight: bold; font-size: 120%; } #options-center { background-color: #f4f4f4 !important; border-bottom: 1px solid #aaa !important; padding: 10px; border-top: 1px solid #aaa; } #options fieldset { margin: 10px 0; border: none; border-top: 1px solid #ccc; padding: 10px; } #options fieldset legend { font-weight: bold; color: #333; padding: 0 5px; text-shadow: 0 1px rgba(255,255,255,0.9); } #options-selector-table { margin: 0; padding: 0; width: 100%; border-collapse: collapse; } #options-selector-table td { padding: 2px 5px; } #options-selector-table select { height: 25px; } #options-selector-table input[type=text] { border: 1px solid #999; border-radius: 5px; -webkit-box-shadow: inset 1px 2px 3px rgba(0,0,0,0.2); padding: 5px 5px; } #options-language-help a { background-image: url('../img/question-small-white.png'); background-repeat: no-repeat; background-position: 0 -2px; padding-left: 20px; min-height: 16px; display: inline-block; vertical-align: middle; color: #666; text-decoration: none; } #options-language-help a:hover { text-decoration: underline; } #options-selector { width: 100%; } #options-attributes { width: 100%; border-collapse: collapse; border-spacing: 0; } #options-attributes tbody td { padding: 4px; } #options-attributes thead tr { border: 1px solid #ccc; } #options-attributes thead th { background: -webkit-gradient(linear, left bottom, left top, color-stop(0.1, #ccc), color-stop(0.8, #eee)); border: none; color: #333; padding: 4px; text-align: left; text-shadow: 0 1px rgba(255,255,255,0.9); } #options-attributes .dragHandle { width: 16px; cursor: move; background-image: url('../img/handle.png'); background-position: left center; background-repeat: no-repeat; } #options-attributes tr.tDnD_whileDrag { opacity: 0.5; } #options-attributes tr.tDnD_whileDrag .dragHandle { background-position: left center; background-repeat: no-repeat; } #options-attributes input { width: 100%; border: none; border-radius: 0; -webkit-box-shadow: none; padding: 0; background: transparent; padding: 4px 2px; } #options-attributes input:focus { border: 1px solid #aaa; padding: 3px 1px; background-color: #fff; } #options-attributes img { margin-right: 5px; } #options-presets-select { width: 50%; } #center { margin-left: 380px; border-top: 1px solid #aaa; } #results-table { border-bottom: 1px solid #aaa; } #results-table table { width: 100%; border-spacing: 0; } #results-table table thead { border-bottom: 1px solid #aaa; background: -webkit-gradient(linear, left bottom, left top, color-stop(0.1, #ccc), color-stop(0.8, #eee)); color: #333; text-shadow: 0 1px rgba(255,255,255,0.9); } #results-table table thead tr { position: relative; top: 0; } #results-table table thead th { padding: 5px; border-left: 1px solid #fff; border-right: 1px solid #aaa; border-bottom: 1px solid #aaa; cursor: pointer; } #results-table table td { padding: 5px; } #results-table td.tools, #results-table td.index { width: 10px; } #results-table table td.tools img { margin-right: 2px; cursor: pointer; } #results-table table tr.odd { background-color: #f0f0f0; } #results-table table tr:hover td { background-color: #f5f5ff; } #export { border-top: 1px solid #fff !important; background: -webkit-gradient(linear, left bottom, left top, color-stop(0.1, #ccc), color-stop(0.8, #eee)); padding: 10px; text-align: right; height: 30px; } #about { display: none; padding-top: 10px; background-image: url('../img/scraper48.png'); background-repeat: no-repeat; background-position: 10px 10px; padding-left: 70px; } #about h1 { margin: 0; font-size: 200%; font-weight: normal; margin-bottom: 0.2em; } #about h2 { font-size: 100%; margin-bottom: 2em; } #about dl { margin-top: 2em; } #about dl dt { font-weight: bold; margin-bottom: 0.5em; } #about dl dd { padding-left: 1em; margin-bottom: 0.5em; } #about a { text-decoration: underline; } .pane-footer { height: 30px; border-top: 1px solid #fff !important; background: -webkit-gradient(linear, left bottom, left top, color-stop(0.1, #ccc), color-stop(0.8, #eee)); padding: 10px; } .pane-footer table { width: 100%; margin: 0; padding: 0; border: 0; border-collapse: collapse; } .pane-footer table td { margin: 0; padding: 0; text-align: right; } .pane-footer table td:first-child { text-align: left; } .ui-corner-all { border-radius: 0; } .ui-tabs { padding: 0; } .ui-state-default a { display: inline-block; vertical-align: top; } .ui-state-default a img { margin-right: 5px; } /* * PANES & CONTENT-DIVs */ .ui-layout-pane { background: #FFF; border: 1px solid #BBB; overflow: auto; } .ui-layout-content { position: relative; overflow: auto; } /* * RESIZER-BARS */ .ui-layout-resizer { /* all 'resizer-bars' */ background: #eee; border-right: 1px solid #ddd !important; border-left: 1px solid #fff !important; border-width: 0; } .ui-layout-resizer-drag { /* REAL resizer while resize in progress */ } .ui-layout-resizer-hover { /* affects both open and closed states */ } /* NOTE: It looks best when 'hover' and 'dragging' are set to the same color, otherwise color shifts while dragging when bar can't keep up with mouse */ .ui-layout-resizer-open-hover , /* hover-color to 'resize' */ .ui-layout-resizer-dragging { /* resizer beging 'dragging' */ background: rgba(255,255,255,0.5); } .ui-layout-resizer-dragging { /* CLONED resizer being dragged */ border-right: 1px solid #ddd !important; border-left: 1px solid #fff !important; } /* NOTE: Add a 'dragging-limit' color to provide visual feedback when resizer hits min/max size limits */ .ui-layout-resizer-dragging-limit { /* CLONED resizer at min or max size-limit */ background: #E1A4A4; /* red */ } .ui-layout-resizer-closed-hover { /* hover-color to 'slide open' */ background: #EBD5AA; } .ui-layout-resizer-sliding { /* resizer when pane is 'slid open' */ opacity: .10; /* show only a slight shadow */ filter: alpha(opacity=10); } .ui-layout-resizer-sliding-hover { /* sliding resizer - hover */ opacity: 1.00; /* on-hover, show the resizer-bar normally */ filter: alpha(opacity=100); } /* sliding resizer - add 'outside-border' to resizer on-hover * this sample illustrates how to target specific panes and states */ .ui-layout-resizer-north-sliding-hover { border-bottom-width: 1px; } .ui-layout-resizer-south-sliding-hover { border-top-width: 1px; } .ui-layout-resizer-west-sliding-hover { border-right-width: 1px; } .ui-layout-resizer-east-sliding-hover { border-left-width: 1px; } /* * TOGGLER-BUTTONS */ .ui-layout-toggler { border: 1px solid #ddd; /* match pane-border */ background-color: #ddd; } .ui-layout-resizer-hover .ui-layout-toggler { opacity: .60; filter: alpha(opacity=60); } .ui-layout-resizer-hover .ui-layout-toggler-hover { /* need specificity */ background-color: #FC6; opacity: 1.00; filter: alpha(opacity=100); } .ui-layout-toggler-north , .ui-layout-toggler-south { border-width: 0 1px; /* left/right borders */ } .ui-layout-toggler-west , .ui-layout-toggler-east { border-width: 1px 0; /* top/bottom borders */ } /* hide the toggler-button when the pane is 'slid open' */ .ui-layout-resizer-sliding ui-layout-toggler { display: none; } /* * style the text we put INSIDE the togglers */ .ui-layout-toggler .content { color: #666; font-size: 12px; font-weight: bold; width: 100%; padding-bottom: 0.35ex; /* to 'vertically center' text inside text-span */ } .sorting_asc { background: url('../img/control-090-small.png') no-repeat center right; } .sorting_desc { background: url('../img/control-270-small.png') no-repeat center right; } .sorting { /* background: url('../images/sort_both.png') no-repeat center right;*/ } .sorting_asc_disabled { background: url('../img/control-090-small.png') no-repeat center right; } .sorting_desc_disabled { background: url('../img/control-270-small.png') no-repeat center right; } ================================================ FILE: src/js/background.js ================================================ /* * background.js * * Author: dave@bit155.com * * --------------------------------------------------------------------------- * * Copyright (c) 2010, David Heaton * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * * Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * * Neither the name of bit155 nor the names of its contributors * may be used to endorse or promote products derived from this software * without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ // oauth var oauth = ChromeExOAuth.initBackgroundPage({ 'request_url': 'https://www.google.com/accounts/OAuthGetRequestToken', 'authorize_url': 'https://www.google.com/accounts/OAuthAuthorizeToken', 'access_url': 'https://www.google.com/accounts/OAuthGetAccessToken', 'consumer_key': 'anonymous', 'consumer_secret': 'anonymous', 'scope': 'https://docs.google.com/feeds/', 'app_name': 'Scraper' }); chrome.extension.onRequest.addListener(function(request, sender, sendResponse) { var command = request.command; var payload = request.payload; if (command === 'scraperScrapeTab') { // forward requests for "scraperScrape" to the appropriate tab chrome.tabs.sendRequest(parseInt(payload.tab, 10), { command: 'scraperScrape', payload: payload.options }, sendResponse); } else if (command === 'scraperSpreadsheet') { // export spreadsheet to google docs oauth.authorize(function() { // remove trailing colons from slug as this will result in error due to // http://code.google.com/a/google.com/p/apps-api-issues/issues/detail?id=2136 var title = payload.title || ''; var slug = encodeURIComponent(title.replace(/[:]+\s*$/,'')); var request = { 'method': 'POST', 'headers': { 'GData-Version': '3.0', 'Content-Type': 'text/csv', 'Slug': slug }, 'parameters': { 'alt': 'json' }, 'body': payload.csv }; var url = 'https://docs.google.com/feeds/default/private/full'; var callback = function(response, xhr) { if (xhr.status == 401) { // unauthorized, token probably bad so clear it oauth.clearTokens(); sendResponse({error: 'Google authentication failed. Please try exporting again, and you will be re-authenticated.'}); } else if (xhr.status - 200 < 100) { try { var json = JSON.parse(response); // open page if (json && json.entry && json.entry.link) { var links = json.entry.link; for (var i = 0; i < links.length; i++) { if (links[i].rel === 'alternate' && links[i].type === 'text/html') { chrome.tabs.create({ url: links[i].href }); } } } // forward response to the caller sendResponse(json); } catch (error) { sendResponse({ error: error }); } } else { sendResponse({ error: 'Received an unexpected response.\n\n' + response }); } }; oauth.sendSignedRequest(url, callback, request); }); } }); // make some default presets if (!bit155.scraper.presets()) { bit155.scraper.presets([ { name: 'Paragraph Text', options: { language: 'xpath', selector: '//p', attributes: [ { xpath: '.', name: 'Text' } ], filters: [ 'empty' ] } }, { name: 'Links', options: { language: 'xpath', selector: '//a', attributes: [ { xpath: '.', name: 'Link' }, { xpath: '@href', name: 'URL' } ], filters: ['empty'] } } ]); }; // context menus var scrapeSimilarItem = chrome.contextMenus.create({ title: "Scrape similar...", contexts: ['all'], onclick: function(info, tab) { var active = false; // get selection options and open viewer with the response chrome.tabs.sendRequest(tab.id, { command: 'scraperSelectionOptions' }, function(response) { active = true; bit155.scraper.viewer(tab, response); }); // offer to reload page if no response setTimeout(function() { if (!active && confirm('You need to reload this page before you can use Scraper. Press ok if you would like to reload it now, or cancel if not.')) { chrome.tabs.update(tab.id, {url: "javascript:window.location.reload()"}); } }, 500); } }); ================================================ FILE: src/js/bit155/attr.js ================================================ /* * attr.js * * Author: dave@bit155.com * * --------------------------------------------------------------------------- * * Copyright (c) 2010, David Heaton * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * * Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * * Neither the name of bit155 nor the names of its contributors * may be used to endorse or promote products derived from this software * without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ var bit155 = bit155 || {}; /** * Creates an attribute accessor function. * * If one argument is passed to the function, then the value will be assigned * to the attribute. * * If multiple arguments are passed, then they will be stored in an array and * the array will be assigned to the attribute. * * Otherwise, if no arguments are provided, then the value of the attribute is * returned. * * @param initial {any} the initial value, will NOT be passed through the * filter * @param filter {function(newValue, oldValue)} (optional) function called * before assigning a new value, which returns a filtered version of * the value * @param callback {function(newValue, oldValue)} (optional) function called * after assigning a new value */ bit155.attr = function(options) { var _value = options ? options.initial : null; var filter = options ? options.filter : false; var callback = options ? options.callback : false; return function() { var newValue, oldValue; if (arguments.length > 0) { if (arguments.length === 1) { newValue = arguments[0]; } else { var i; newValue = []; for (i = 0; i < arguments.length; i++) { newValue.push(arguments[i]); } } // filter value oldValue = _value; if (filter) { var filteredValue = filter.call(this, newValue, oldValue); if (filteredValue !== undefined) { newValue = filteredValue; } } // copy new value if (typeof newValue === 'object') { if ($.isArray(newValue)) { _value = $.extend(true, [], newValue); } else { _value = $.extend(true, {}, newValue); } } else { _value = newValue; } if (callback) { callback.call(this, newValue, oldValue); } return this; } return _value; }; }; ================================================ FILE: src/js/bit155/csv.js ================================================ /* * csv.js * * Author: dave@bit155.com * * --------------------------------------------------------------------------- * * Copyright (c) 2010, David Heaton * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * * Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * * Neither the name of bit155 nor the names of its contributors * may be used to endorse or promote products derived from this software * without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ var bit155 = bit155 || {}; bit155.csv = bit155.csv || {}; /** * Encodes a CSV cell. * @param cell {string} cell to encode */ bit155.csv.cell = function(cell) { var str; if (cell === undefined || cell === null) { return ""; } else if (typeof cell === 'string') { str = cell; } else { str = cell.toString(); } if (str.match(/[,"\n\r]/)) { str = str.replace(/(["])/g, '"$1'); str = '"' + str + '"'; } return str; }; /** * Encodes an array as a CSV row. Accepts an array of values or you can pass * variable arguments to it. * * @param row (any) a single array of values or any number of variable * arguments */ bit155.csv.row = function() { var row, text = '', i; if (arguments.length === 1) { row = $.isArray(arguments[0]) ? arguments[0] : arguments; } else { row = arguments; } for (i = 0; i < row.length; i++) { if (i > 0) { text += ','; } text += bit155.csv.cell(row[i]); } return text; }; bit155.csv.csv = function(data) { var text = ''; var i; if (!$.isArray(data)) { return ""; } for (i = 0; i < data.length; i++) { text += bit155.csv.row(data[i]) + '\n'; } return text; }; ================================================ FILE: src/js/bit155/scraper.js ================================================ /* * scraper.js * * Author: dave@bit155.com * * --------------------------------------------------------------------------- * * Copyright (c) 2010, David Heaton * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * * Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * * Neither the name of bit155 nor the names of its contributors * may be used to endorse or promote products derived from this software * without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ var bit155 = bit155 || {}; bit155.scraper = bit155.scraper || {}; /** * Function that creates a new viewer window bound to the specified tab. * * @param {Object} tab (optional) the tab object to bind the viewer to * (defaults to the currently selected tab) * @param {Object} options (optional) options to initialize viewer with */ bit155.scraper.viewer = function(tab, options) { options = options || {}; // call this again with selected tab if none specified if (!tab) { chrome.tabs.getSelected(undefined, function(tab) { if (tab) { bit155.scraper.viewer(tab, options); } }); return; } // can't work on extensions pages if (tab.url.indexOf("https://chrome.google.com/extensions") == 0 || tab.url.indexOf("chrome://") == 0) { alert("Scraper is not permitted to work on the Google Chrome extensions page for security reasons."); return; } // open window if we get a ping response chrome.windows.create({ url: chrome.extension.getURL('viewer.html') + "?tab=" + tab.id + "&options=" + encodeURIComponent(JSON.stringify(options)), type: 'popup', width: Math.max(650, parseInt((localStorage['viewer.width'] || '960'), 10)), height: Math.max(250, parseInt((localStorage['viewer.height'] || '400'), 10)) }); }; /** * Contains presets, backed by localStorage['presets']. Contains migration * from the old localStorage['viewer.presets'] since this attribute has * larger scope than just the viewer. */ bit155.scraper.presets = bit155.attr({ initial: JSON.parse(localStorage['presets'] || localStorage['viewer.presets'] || 'null'), filter: function(v) { if (v && !$.isArray(v)) { throw new Error('Preset must be an array.'); } return v; }, callback: function(v) { localStorage['presets'] = v ? JSON.stringify(v) : null; } }); /** * Generates an xpath that is specific, but hopefully not too specific, for * a node. * * @param {Object} node to generate xpath for */ bit155.scraper.xpathForNode = function(node) { var xpath = $(node).xpath(), xpathLastPredicateRegex = /^(.*)(\[\d+\])([^\[\]]*)$/, xpathFirstSegmentRegex = /^(\/+[^\/]+)(.*)$/, result, selection, selectionTrimmed; // keep cutting out the last predicate until we match more than one node // and consider this our ideal selection while ((result = xpathLastPredicateRegex.exec(xpath))) { selection = bit155.scraper.select(document, xpath, 'xpath'); if (selection.length > 1) { break; } xpath = result[1] + result[3]; } if (!selection) { return xpath; } // trim the front of the path until we have smallest xpath that returns // same number of elements while ((result = xpathFirstSegmentRegex.exec(xpath))) { selectionTrimmed = bit155.scraper.select(document, '/' + result[2], 'xpath') || []; if (selectionTrimmed.length !== selection.length) { break; } xpath = '/' + result[2]; } return xpath; }; /** * Generates bit155.scraper.scrape options for the given selection. Uses magic * to try and guess reasonable defaults. * * @param {Object} focusNode same semantics as Selection.focusNode * @param {Object} anchorNode (optional) same as Selection.anchorNode * @param {HTMLDocument} doc the document in which to match */ bit155.scraper.optionsForSelection = function(focusNode, anchorNode, doc) { var options = {}, ancestor, ancestorTagName, ancestorClassName, node; doc = doc || window.document; // determine common ancestor based on user's current selection if (anchorNode) { ancestor = $([focusNode, anchorNode]).commonAncestor(); } else { ancestor = $(focusNode).closest('*'); } // tweak ancestor for some types of elements // XXX design if (ancestor && ancestor.length > 0) { ancestorTagName = ancestor.get(0).tagName.toLowerCase(); if (ancestorTagName === 'table' || ancestorTagName === 'tbody' || ancestorTagName === 'thead' || ancestorTagName === 'tfoot') { // table? select rows instead ancestor = $(focusNode).closest('tr'); } else if (ancestorTagName === 'dl') { // dl? select terms instead ancestor = ancestor.find('dt').first(); } else if (ancestorTagName === 'ul' || ancestorTagName === 'ol') { // dl? select terms instead ancestor = ancestor.find('li').first(); } } // populate options options.language = 'jquery'; options.selector = ''; options.attributes = []; if (ancestor && ancestor.length > 0) { node = ancestor.get(0); ancestorTagName = node.tagName.toLowerCase(); ancestorClassName = $.trim(node.className); options.selector = ancestorTagName; // find first xpath that matches more than one element by removing the // index selector from each xpath segment. biggest caveats: // // * only selecting elements with same structure // * won't work when selecting an outlier with deeper structure than peers // * ignores semantics // options.language = 'xpath'; options.selector = bit155.scraper.xpathForNode(node); // use "magical" attributes depending on what custom ancestor is if (ancestorTagName === 'tr') { var headers = (function() { var table = ancestor.closest('table'); var columns = ancestor.children().length; var firstRow = table.find('tr').first(); var headerRow; // find first row in the table, and if it contains the same number of // TH cells as data cells in our TR ancestor, then assume it contains // column names if (firstRow && firstRow.children('th').length == columns) { headerRow = firstRow; } else { headerRow = ancestor; } return headerRow.children().map(function(index, cell) { if (cell.tagName === 'TH') { return $(cell).text(); } else { return 'Column ' + (index + 1); } }); })(); // create an attribute for each header $.each(headers, function(index,name) { options.attributes.push({ xpath: '*[' + (index + 1) + ']', name: name }); }); // append a [td] constraint to the selector so that we don't scrape // rows containing only headers options.selector = options.selector + "[td]"; } else if (ancestorTagName === 'a') { options.attributes.push({ xpath: '.', name: 'Link' }); options.attributes.push({ xpath: '@href', name: 'URL' }); } else if (ancestorTagName === 'img') { options.attributes.push({ xpath: '@title', name: 'Title' }); options.attributes.push({ xpath: '@src', name: 'Source' }); } else if (ancestorTagName === 'dt') { options.attributes.push({ xpath: '.', name: 'Term' }); options.attributes.push({ xpath: './following-sibling::dd', name: 'Definition' }); } else { options.attributes.push({ xpath: '.', name: 'Text' }); } } return options; }; /** * Selects elements using a selector string in some language. * * @param {node} context what to search * @param {string} selector the query string * @param {string} language what language ("jquery" or "xpath") the selector * is expressed in */ bit155.scraper.select = function(context, selector, language) { if (typeof context !== 'object') { throw "Context object is required."; } if (typeof selector !== 'string') { throw "Selector string is required."; } if (language === 'xpath') { // https://developer.mozilla.org/en/XPathResult // http://stackoverflow.com/questions/727902/jquery-select-text var xpr = document.evaluate(selector, context || document, null, XPathResult.ANY_TYPE, null); var i, item, result = []; for (i = 0; item = xpr.iterateNext(); i++) { result.push(item); } return $(result); } else if (language === 'jquery') { return $(context).find(selector); } else { throw new Error('Unsupported selector language: ' + language); } }; /** * Scrapes a page. */ bit155.scraper.scrape = function(options) { var selector = options['selector']; var attributes = options['attributes'] || []; var filters = options.filters || []; var result = []; // make sure xpath in each attribute $.each(attributes, function() { if (!this.xpath) { throw new Error("XPath is required for each attribute."); } }); // collect results bit155.scraper.select(document, options.selector, options.language).each(function(i,e) { var el = $(e); var values = []; var include = true; if (attributes) { var xpathResult = null; $.each(attributes, function() { values.push(document.evaluate(this.xpath, e, null, XPathResult.STRING_TYPE, null).stringValue); }); } result.push({ 'xpath': el.xpath(), 'values': values }); }); // apply filters $.each(filters, function(i,filter) { if (filter === 'empty') { result = result.filter(function(result) { for (var i = 0; i < result.values.length; i++) { if ($.trim(result.values[i]) !== '') { return true; } } return false; }); } }); return result; }; ================================================ FILE: src/js/contentscript.js ================================================ /* * contentscript.js * * Author: dave@bit155.com * * --------------------------------------------------------------------------- * * Copyright (c) 2010, David Heaton * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * * Neither the name of bit155 nor the names of its contributors * may be used to endorse or promote products derived from this software * without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ (function(){ // listen for context menu var contextNode; addEventListener("contextmenu", function(e) { contextNode = e.srcElement; }); // listen for requests chrome.extension.onRequest.addListener(function(request, sender, sendResponse) { var command = request.command, payload = request.payload, response = $.extend({}, payload); try { if (command === 'scraperScrape') { // scrape response.result = bit155.scraper.scrape(response); } else if (command === 'scraperSelectionOptions') { // selection options (function(){ var focusNode, anchorNode, selectionDocument, selection; // abort if no contextNode as probably being invoked from another // frame if (!contextNode) { response.error = "Frames are not supported at the moment. Please open the frame in a new tab or window and try scraping again."; return; } // determine range of selection selection = window.getSelection(); selectionDocument = window.document; if (selection.isCollapsed) { // nothing selected, so use whatever node is under the cursor focusNode = contextNode; } else { // select focus and anchor nodes from selection focusNode = selection.focusNode; anchorNode = selection.anchorNode; } // clear context node contextNode = null; // extend response with options generated from current selection response = $.extend(response, bit155.scraper.optionsForSelection(focusNode, anchorNode, selectionDocument)); }()); } else if (command === 'scraperHighlight') { // highlight (function() { var elements; if (payload.selector) { elements = bit155.scraper.select(document, payload.selector, payload.language); } else if (payload.xpath) { elements = bit155.scraper.select(document, payload.xpath, 'xpath'); } else if (payload.jquery) { elements = $(payload.jquery); } if (elements) { window.scrollTo(elements.offset().left, elements.offset().top); elements.filter(':visible').effect('highlight', {}, 'slow'); } }()); } else if (command === 'scraperPing') { // ping } else { throw new Error('Unsupported request: ' + JSON.stringify(request)); } } catch (error) { console.error(error); response.error = error; } sendResponse(response); }); }()); ================================================ FILE: src/js/popup.js ================================================ /* * popup.js * * Author: dave@bit155.com * * --------------------------------------------------------------------------- * * Copyright (c) 2010, David Heaton * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * * Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * * Neither the name of bit155 nor the names of its contributors may be * used to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ $(function() { var presets = bit155.scraper.presets(), presetList = $('#presets'); $('.viewer').click(function() { bit155.scraper.viewer(); return false; }); if (presets.length === 0) { presetList.append($('
  • ').text("No presets have been defined yet.")); } else { $.each(presets, function(index, preset) { presetList.append($('
  • ').append($('').text(preset.name).click(function() { bit155.scraper.viewer(null, preset.options); return false; }))); }); } }); ================================================ FILE: src/js/shared.js ================================================ /* * shared.js * * Author: dave@bit155.com * * --------------------------------------------------------------------------- * * Copyright (c) 2010, David Heaton * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * * Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * * Neither the name of bit155 nor the names of its contributors * may be used to endorse or promote products derived from this software * without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ // $(...).serializeParams() // -------------------------------------------------------------------------- /** * Plugin that generates an object containing parameters of a form. Allows * Railsesque multi-dimensional parameter names (eg. 'user[name]' and * 'users[][name]'). */ (function($){ /** * Splits a parameter name into an array of tokens. Empty brackets are * tokenized as `undefined` and indicate that the preceding token should be * considered an array. */ function _parseParam(str) { var path = []; var s = str; // first match expects no brackets, subsequent matches do for (var match = s.match(/^([^\[]*)(.*)$/); match; match = s.match(/^\[([^\]]*)\](.*)$/)) { path.push(match[1] || undefined); s = match[2]; } // if there is a remaining string, then it was probably malformed, so warn // user and return a single-segment path consisting of just str (so that // code doesn't break) if (s) { if (console && console.warn) { console.warn('Malformed path: ' + str); } return [str]; } return path; } $.fn.serializeParams = function() { var result = {}; $.each($(this).serializeArray(), function() { var keys = _parseParam(this.name); var containers = [result]; for (var i = 0; i < keys.length; i++) { var container = containers[containers.length - 1]; var key = keys[i]; var keyIndicatesArray = keys[i+1] === undefined; var leaf = (i === keys.length - 1); // handle four cases: if (leaf && key === undefined) { // LEAF ARRAY container.push(this.value); } else if (leaf) { // LEAF OBJECT // if nothing already defined here, assign value, otherwise we need // to convert existing value into an array and append or find an if (container[key] === undefined) { container[key] = this.value; } else { // collision! create an array here or add new element to an // ancestor array var lastUndefinedKey = keys.lastIndexOf(undefined); if (lastUndefinedKey < 0) { container[key] = [container[key], this.value]; } else { for (; i >= 0; i--) { if (keys[i] === undefined) { break; } containers.pop(); } containers[containers.length - 1].push({}); i = i - 1; continue; } } } else if (key === undefined) { // INNER ARRAY if (container.length === 0) { container.push({}); } containers.push(container[container.length - 1]); } else { // INNER NODE if (container[key] === undefined) { container[key] = (keyIndicatesArray) ? [] : {}; } containers.push(container[key]); } } }); return result; }; }(jQuery)); // $(...).xpath // -------------------------------------------------------------------------- /** * Returns the xpath of an element. */ (function($){ $.fn.xpath = function(options) { var node; var path = ""; var tag, segment, siblings; for (node = this.get(0); node && node.nodeType == 1; node = node.parentNode) { tag = node.tagName.toLowerCase(); segment = tag; // append index siblings = $(node).parent().children(tag); if (siblings.length > 1) { path = "/" + tag + "[" + (siblings.index(node) + 1) + "]" + path; } else { path = "/" + tag + path; } } return path; }; }(jQuery)); // $(...).cssSelector // -------------------------------------------------------------------------- /** * The CSS selector plugin lets you generate CSS selectors of elements. */ (function($){ var component = function(el, options) { var str = ''; if (options.tagName && el.tagName) str = str + el.tagName; if (options.id && el.id) str = str + '#' + el.id; if (options.classes && el.className) str = str + '.' + el.className.split(/\s+/).join('.'); if (options.attributes && el.attributes) { $.each(el.attributes, function(i, attr) { str = str + '[' + attr.name + "='" + attr.value + "']"; }); } return str; }; var path = function(el, options) { var path = []; path.push(component(el, options)); el.parents().each(function(i,el) { path.push(component(this, options)); }).get(); return path.reverse(); }; $.fn.cssSelector = function(options) { var settings = { tagName: true, id: true, classes: true, attributes: false }; if (options) { $.extend(settings, options); } return path(this, settings).filter(function(e,i,a) { return e; }).join(' > '); }; }(jQuery)); // $(...).commonAncestor // -------------------------------------------------------------------------- (function($) { $.fn.commonAncestor = function() { // http://stackoverflow.com/questions/3217147/jquery-first-parent-containing-all-children var parents = []; var minlen = Infinity; var i; $(this).each(function() { var curparents = $(this).parents(); parents.push(curparents); minlen = Math.min(minlen, curparents.length); }); for (i in parents) { parents[i] = parents[i].slice(parents[i].length - minlen); } // Iterate until equality is found for (i in parents[0]) { var equal = true; for (var j in parents) { if (parents[j][i] != parents[0][i]) { equal = false; break; } } if (equal) { return $(parents[0][i]); } } return $([]); }; }(jQuery)); ================================================ FILE: src/js/viewer.js ================================================ /* * viewer.js * * Author: dave@bit155.com * * --------------------------------------------------------------------------- * * Copyright (c) 2010, David Heaton * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * * Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * * Neither the name of bit155 nor the names of its contributors * may be used to endorse or promote products derived from this software * without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ /** * Sort of a controller class for the viewer and work-in-progress towards a * real architecture. Mostly so I can test some of the logic in it. */ var Viewer = function() {}; /** * Contains the scrape data. */ Viewer.prototype.data = bit155.attr({ callback: function(v) { this.reload(); } }); /** * Contains the id of the tab this is connected to. */ Viewer.prototype.tabId = bit155.attr({ filter: function(v) { return parseInt(v, 10); } }); /** * Reloads data of elements dependent on bit155.scraper.presets data. */ Viewer.prototype.reloadPresets = function() { var list; var self = this; var presets = bit155.scraper.presets(); // update list list = $('#presets-list'); list.empty(); $.each(presets || [], function(i, preset) { var handle = $(''); var load = $('').text(preset.name).click(function() { self.options(preset.options); $('#presets-form-name').val(preset.name); $('#presets').dialog('close'); self.scrape(); return false; }); var remove = $('').append($('')).click(function() { if (confirm('Are you sure you want to remove the preset, "' + preset.name + '"?')) { presets.splice(i,1); bit155.scraper.presets(presets); self.reloadPresets(); } }); list.append($('
  • ').attr('id', 'preset-' + i).append(handle).append(remove).append(load)); }); }; /** * Returns the current options (as encapsulated by the form) or sets new * options. * * @param opts {object} (optional) new options to set */ Viewer.prototype.options = function(opts) { if (opts) { var self = this; // selector and language $('#options-selector').val(opts.selector).change(); $('#options-language').val(opts.language).change(); // attributes if ($.isArray(opts.attributes) && opts.attributes.length > 0) { $('#options-attributes tbody').empty(); $.each(opts.attributes, function() { self.addAttribute(this.xpath, this.name); }); } else { self.addAttribute('.', 'Text'); } // filters $('#options-filters').find('input:checkbox').attr('checked', false); if ($.isArray(opts.filters) && opts.filters.length > 0) { $.each(opts.filters, function(index, filter) { if (filter === 'empty') { $('#options-filters-empty').attr('checked', true); } }); } return this; } else { return $('#options').serializeParams(); } }; /** * Adds an attribute to the options. * * @param xpath {string} * @param name {string} * @param context {element} another row to add attribute beneath, or null if * it should be appended to the end of the table */ Viewer.prototype.addAttribute = function(xpath, name, context) { var self = this; var xpathInput = $('').attr('type', 'text').attr('name', 'attributes[][xpath]').attr('placeholder', 'XPath').val(xpath || ''); var nameInput = $('').attr('type', 'text').attr('name', 'attributes[][name]').attr('placeholder', 'Name (optional)').val(name || ''); var row = $(''); var addRow = function() { self.addAttribute('', '', row); return false; }; var deleteRow = function() { var parent = row.parent(); if (parent.children().length > 1) { row.fadeOut('fast', function() { row.remove(); }); } else { xpathInput.val(''); nameInput.val(''); } return false; }; // create row row.append($('').addClass('dragHandle').text(' ')); row.append($('').append(xpathInput)); row.append($('').append(nameInput)); row.append($('') .append($('').attr('href', 'javascript:;').click(deleteRow).html('')) .append($('').attr('href', 'javascript:;').click(addRow).html('')) ); row.hide(); // insert row var after = function() { $('#options-attributes').tableDnD({ dragHandle: 'dragHandle' }); }; if (context) { context.after(row.fadeIn('fast', after)); } else { $('#options-attributes tbody').append(row.fadeIn('fast', after)); } }; /** * Displays an error message. * * @param {Object} an error object containing a "message" property, or a * string, to display */ Viewer.prototype.error = function(error) { $('
    ').text(error.message ? error.message : '' + error).dialog({ title: 'Error', modal: true, buttons: [{ text: "Close", click: function() { $(this).dialog("close"); } }] }); }; /** * Reloads the view based on current data. */ Viewer.prototype.reload = function() { var self = this; var data = this.data(); var results = data.result || []; var attributes = data.attributes || []; // headers var thead = $(''); var headerRow = $('').appendTo(thead).append(' ').append(' '); $.each(attributes, function() { headerRow.append($('').text(this.name)); }); // body var tbody = $(''); $.each(results, function(i,result) { var row = $('').appendTo(tbody); var tools = $('').appendTo(row); // tools tools.append($('').click(function() { chrome.tabs.sendRequest(self.tabId(), { command: 'scraperHighlight', payload: { xpath: result.xpath } }); })); // index row.append($('').text(i + 1)); // attributes $.each(attributes, function(j,attribute) { var value = result.values[j]; var cell = $('').text(value); row.append(cell); }); }); var url = /^https?:\/\/[^\s]+$/i; var table = $('').append(thead).append(tbody).appendTo($('#results-table').empty()); table.dataTable({ 'bInfo': false, 'bFilter': false, 'bStateSave': true, 'bPaginate': false, 'fnRowCallback': function(row, values, displayIndex, displayIndexFull) { $('td', row).each(function() { var text = $(this).text(); if (url.test(text)) { $(this).empty().append($('').attr('href', text).attr('target', '_blank').text(text)); } }); return row; }, 'aoColumnDefs': [ { aTargets: [0], bSortable: false } ] }); }; /** * Scrapes the host document using the current options. */ Viewer.prototype.scrape = function() { var self = this; var options = self.options(); // clean out empty attributes and set names for empties options.attributes = $.map(options.attributes.filter(function(a) { return a.xpath !== ''; }), function(a) { if (!a.name) { a.name = a.xpath; } return a; }); var request = { command: 'scraperScrapeTab', payload: { tab: self.tabId(), options: options } }; chrome.extension.sendRequest(request, function(response) { if (response.error) { self.error(response.error); } self.data(response); }); }; /** * Creates a Google spreadsheet with the current data. */ Viewer.prototype.spreadsheet = function() { var self = this; var data = []; var csv; // gather up data and convert to csv data.push($.map(this.data().attributes || [], function(a) { return a.name || a.xpath; })); $.each(this.data().result || [], function(index, result) { data.push(result.values); }); csv = bit155.csv.csv(data); // find the host tab so we can get its title chrome.tabs.get(self.tabId(), function(tab) { var request = {}; var dialog = $('
    ').addClass('progress'); var title = tab.title; // ask user for title // title = prompt('Please enter a title for your Google spreadsheet:', title); // if (!title) { // return; // } // tell user to wait dialog.append($('
    ')); dialog.dialog({ closeOnEscape: true, buttons: [], resizable: false, title: 'Exporting to Google Docs...', modal: true }); // send spreadsheet request to background.js request.command = 'scraperSpreadsheet'; request.payload = { title: title, csv: csv }; chrome.extension.sendRequest(request, function(response) { dialog.dialog('close'); if (response.error) { self.error(response.error); } }); }); }; // from http://safalra.com/web-design/javascript/parsing-query-strings/ function parseQueryString(_1){var _2={};if(_1==undefined){_1=location.search?location.search:"";}if(_1.charAt(0)=="?"){_1=_1.substring(1);}_1=_1.replace(/\+/g," ");var _3=_1.split(/[&;]/g);for(var i=0;i<_3.length;i++){var _5=_3[i].split("=");var _6=decodeURIComponent(_5[0]);var _7=decodeURIComponent(_5[1]);if(!_2[_6]){_2[_6]=[];}_2[_6].push((_5.length==1)?"":_7);}return _2;} $(function() { var response = parseQueryString(), responseOptions = response.options && response.options.length > 0 ? JSON.parse(response.options[0]) : {}, savedOptions = JSON.parse(localStorage['viewer.options'] || JSON.stringify({ selector: 'a', language: 'jquery', attributes: [ { xpath: '.', name: 'Link' }, { xpath: '@href', name: 'URL' } ], filters: [ 'empty' ] })), options = $.extend({}, savedOptions, responseOptions); // create viewer var viewer = new Viewer(); viewer.tabId(response.tab && response.tab.length > 0 ? parseInt(response.tab[0], 10) : -1); viewer.options(options); // layout view var layout = $('body').layout({ west: { size: 340, minSize: 250, closable: true, resizable: true, slidable: true } }); if (localStorage['viewer.west.size']) { layout.sizePane('west', localStorage['viewer.west.size']); } if (localStorage['viewer.west.closed'] == 'true') { layout.close('west'); } $('#bottom').accordion({ collapsible: true, active: false, autoHeight: false, animated: false }); $('#center').tabs(); // bind buttons to viewer $('#options').submit(function() { viewer.scrape(); return false; }); $('#export').submit(function() { viewer.spreadsheet(); return false; }); // close the window when the tab is closed chrome.tabs.onRemoved.addListener(function(tabId) { if (tabId == viewer.tabId()) { window.close(); } }); // update title whenever tab changes var updateMeta = function(tab) { document.title = "Scraper - " + tab.title; $('#options-meta-page').empty().append($('
    ').attr('href', tab.url).text(tab.title).click(function() { chrome.tabs.update(viewer.tabId(), { selected: true }); return false; })); // resize content since the header height may change and this buggers up // the footer layout.resizeContent('west'); }; chrome.tabs.get(viewer.tabId(), updateMeta); chrome.tabs.onUpdated.addListener(function(tabId, changeInfo, tab) { if (tabId === viewer.tabId()) { updateMeta(tab); } }); // help dialog $('#about').dialog({ autoOpen: false, draggable: false, resizable: false, title: 'About', width: 400, show: 'fade', hide: 'fade', modal: true, closeText: 'Close', buttons: [{ text: "Close", click: function() { $(this).dialog("close"); } }] }); $('#about-link').click(function() { $('#about').dialog('open'); return false; }); // presets $('#presets').dialog({ autoOpen: false, width: Math.max(100, parseInt(JSON.parse(localStorage['viewer.presets.width'] || '400'), 10)), height: Math.max(100, parseInt(JSON.parse(localStorage['viewer.presets.height'] || '300'), 10)), position: JSON.parse(localStorage['viewer.presets.position'] || '"center"'), modal: true, title: 'Presets', beforeClose: function() { var position = $(this).dialog('option', 'position'); localStorage['viewer.presets.position'] = JSON.stringify([position[0], position[1]]); localStorage['viewer.presets.width'] = $(this).dialog('option', 'width'); localStorage['viewer.presets.height'] = $(this).dialog('option', 'height'); } }); $('#options-presets-button').click(function() { $('#presets').dialog('open'); return false; }); $('#presets-form').submit(function() { var preset = {}; var presetList = bit155.scraper.presets(); var presetForm = $(this).serializeParams(); var options = viewer.options(); var i; // make sure it's a unique name if ($.trim(presetForm.name || '') === '') { viewer.error('You must specify a name for the preset.'); return false; } for (i = 0; i < presetList.length; i++) { if (presetList[i].name === presetForm.name) { if (!confirm('There is already a preset with the name "' + presetForm.name + '". Do you want to overwrite the existing preset?')) { return false; } } } // configure preset preset.name = presetForm.name; preset.options = {}; preset.options.language = options.language; preset.options.selector = options.selector; preset.options.attributes = $.extend(true, [], options.attributes); preset.options.filters = $.extend(true, [], options.filters); // remove existing presets with the same name, append new preset, and save presetList = presetList.filter(function(p) { return p.name !== preset.name; }); presetList.unshift(preset); bit155.scraper.presets(presetList); viewer.reloadPresets(); return false; }); $('#presets-list').sortable({ update: function(event, ui) { var presetMap = {}; var presetList = []; // map existing presets to identifier strings $.each(bit155.scraper.presets(), function(i, p) { presetMap['preset-' + i] = p; }); // reorder the preset list $.each($(this).sortable('toArray'), function(i, id) { presetList.push(presetMap[id]); }); bit155.scraper.presets(presetList); viewer.reloadPresets(); } }); viewer.reloadPresets(); // reset button $('#options-reset-button').click(function() { if (confirm("Do you want to reset the options to their original values?")) { $('#presets-form-name').val(''); viewer.options(options); viewer.scrape(); } return false; }); // language $('#options-language').change(function() { var lang = $('#options-language').val(); $('#options-language-help').empty(); if (lang === 'jquery') { $('#options-language-help').append($('').text('jQuery Reference')); } else if (lang === 'xpath') { $('#options-language-help').append($('').text('XPath Reference')); } }); $('#options-language').change(); // initial scrape viewer.scrape(); // save dimensions upon resize addEventListener('resize', function(event) { localStorage['viewer.width'] = window.outerWidth; localStorage['viewer.height'] = window.outerHeight; }); // save options on close addEventListener("unload", function(event) { var options = viewer.options(); if (!options.filters) { options.filters = []; } localStorage['viewer.options'] = JSON.stringify(options); localStorage['viewer.west.size'] = layout.state.west.size; localStorage['viewer.west.closed'] = layout.state.west.isClosed; }, true); // give selectorinput focus $('#options-selector').select().focus(); setTimeout(function() { layout.resizeAll(); }, 100); // if error, wait a moment to show it if (options.error) { setTimeout(function() { viewer.error(options.error); }, 500); } }); ================================================ FILE: src/lib/datatables-1.7.4/js/jquery.dataTables.js ================================================ /* * File: jquery.dataTables.js * Version: 1.7.4 * Description: Paginate, search and sort HTML tables * Author: Allan Jardine (www.sprymedia.co.uk) * Created: 28/3/2008 * Language: Javascript * License: GPL v2 or BSD 3 point style * Project: Mtaala * Contact: allan.jardine@sprymedia.co.uk * * Copyright 2008-2010 Allan Jardine, all rights reserved. * * This source file is free software, under either the GPL v2 license or a * BSD style license, as supplied with this software. * * This source file is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the license files for details. * * For details please refer to: http://www.datatables.net */ /* * When considering jsLint, we need to allow eval() as it it is used for reading cookies and * building the dynamic multi-column sort functions. */ /*jslint evil: true, undef: true, browser: true */ /*globals $, jQuery,_fnExternApiFunc,_fnInitalise,_fnLanguageProcess,_fnAddColumn,_fnColumnOptions,_fnAddData,_fnGatherData,_fnDrawHead,_fnDraw,_fnReDraw,_fnAjaxUpdate,_fnAjaxUpdateDraw,_fnAddOptionsHtml,_fnFeatureHtmlTable,_fnScrollDraw,_fnAjustColumnSizing,_fnFeatureHtmlFilter,_fnFilterComplete,_fnFilterCustom,_fnFilterColumn,_fnFilter,_fnBuildSearchArray,_fnBuildSearchRow,_fnFilterCreateSearch,_fnDataToSearch,_fnSort,_fnSortAttachListener,_fnSortingClasses,_fnFeatureHtmlPaginate,_fnPageChange,_fnFeatureHtmlInfo,_fnUpdateInfo,_fnFeatureHtmlLength,_fnFeatureHtmlProcessing,_fnProcessingDisplay,_fnVisibleToColumnIndex,_fnColumnIndexToVisible,_fnNodeToDataIndex,_fnVisbleColumns,_fnCalculateEnd,_fnConvertToWidth,_fnCalculateColumnWidths,_fnScrollingWidthAdjust,_fnGetWidestNode,_fnGetMaxLenString,_fnStringToCss,_fnArrayCmp,_fnDetectType,_fnSettingsFromNode,_fnGetDataMaster,_fnGetTrNodes,_fnGetTdNodes,_fnEscapeRegex,_fnDeleteIndex,_fnReOrderIndex,_fnColumnOrdering,_fnLog,_fnClearTable,_fnSaveState,_fnLoadState,_fnCreateCookie,_fnReadCookie,_fnGetUniqueThs,_fnScrollBarWidth,_fnApplyToChildren,_fnMap*/ (function($, window, document) { /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * Section - DataTables variables * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ /* * Variable: dataTableSettings * Purpose: Store the settings for each dataTables instance * Scope: jQuery.fn */ $.fn.dataTableSettings = []; var _aoSettings = $.fn.dataTableSettings; /* Short reference for fast internal lookup */ /* * Variable: dataTableExt * Purpose: Container for customisable parts of DataTables * Scope: jQuery.fn */ $.fn.dataTableExt = {}; var _oExt = $.fn.dataTableExt; /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * Section - DataTables extensible objects * * The _oExt object is used to provide an area where user dfined plugins can be * added to DataTables. The following properties of the object are used: * oApi - Plug-in API functions * aTypes - Auto-detection of types * oSort - Sorting functions used by DataTables (based on the type) * oPagination - Pagination functions for different input styles * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ /* * Variable: sVersion * Purpose: Version string for plug-ins to check compatibility * Scope: jQuery.fn.dataTableExt * Notes: Allowed format is a.b.c.d.e where: * a:int, b:int, c:int, d:string(dev|beta), e:int. d and e are optional */ _oExt.sVersion = "1.7.4"; /* * Variable: sErrMode * Purpose: How should DataTables report an error. Can take the value 'alert' or 'throw' * Scope: jQuery.fn.dataTableExt */ _oExt.sErrMode = "alert"; /* * Variable: iApiIndex * Purpose: Index for what 'this' index API functions should use * Scope: jQuery.fn.dataTableExt */ _oExt.iApiIndex = 0; /* * Variable: oApi * Purpose: Container for plugin API functions * Scope: jQuery.fn.dataTableExt */ _oExt.oApi = { }; /* * Variable: aFiltering * Purpose: Container for plugin filtering functions * Scope: jQuery.fn.dataTableExt */ _oExt.afnFiltering = [ ]; /* * Variable: aoFeatures * Purpose: Container for plugin function functions * Scope: jQuery.fn.dataTableExt * Notes: Array of objects with the following parameters: * fnInit: Function for initialisation of Feature. Takes oSettings and returns node * cFeature: Character that will be matched in sDom - case sensitive * sFeature: Feature name - just for completeness :-) */ _oExt.aoFeatures = [ ]; /* * Variable: ofnSearch * Purpose: Container for custom filtering functions * Scope: jQuery.fn.dataTableExt * Notes: This is an object (the name should match the type) for custom filtering function, * which can be used for live DOM checking or formatted text filtering */ _oExt.ofnSearch = { }; /* * Variable: afnSortData * Purpose: Container for custom sorting data source functions * Scope: jQuery.fn.dataTableExt * Notes: Array (associative) of functions which is run prior to a column of this * 'SortDataType' being sorted upon. * Function input parameters: * object:oSettings- DataTables settings object * int:iColumn - Target column number * Return value: Array of data which exactly matched the full data set size for the column to * be sorted upon */ _oExt.afnSortData = [ ]; /* * Variable: oStdClasses * Purpose: Storage for the various classes that DataTables uses * Scope: jQuery.fn.dataTableExt */ _oExt.oStdClasses = { /* Two buttons buttons */ "sPagePrevEnabled": "paginate_enabled_previous", "sPagePrevDisabled": "paginate_disabled_previous", "sPageNextEnabled": "paginate_enabled_next", "sPageNextDisabled": "paginate_disabled_next", "sPageJUINext": "", "sPageJUIPrev": "", /* Full numbers paging buttons */ "sPageButton": "paginate_button", "sPageButtonActive": "paginate_active", "sPageButtonStaticDisabled": "paginate_button", "sPageFirst": "first", "sPagePrevious": "previous", "sPageNext": "next", "sPageLast": "last", /* Stripping classes */ "sStripOdd": "odd", "sStripEven": "even", /* Empty row */ "sRowEmpty": "dataTables_empty", /* Features */ "sWrapper": "dataTables_wrapper", "sFilter": "dataTables_filter", "sInfo": "dataTables_info", "sPaging": "dataTables_paginate paging_", /* Note that the type is postfixed */ "sLength": "dataTables_length", "sProcessing": "dataTables_processing", /* Sorting */ "sSortAsc": "sorting_asc", "sSortDesc": "sorting_desc", "sSortable": "sorting", /* Sortable in both directions */ "sSortableAsc": "sorting_asc_disabled", "sSortableDesc": "sorting_desc_disabled", "sSortableNone": "sorting_disabled", "sSortColumn": "sorting_", /* Note that an int is postfixed for the sorting order */ "sSortJUIAsc": "", "sSortJUIDesc": "", "sSortJUI": "", "sSortJUIAscAllowed": "", "sSortJUIDescAllowed": "", "sSortJUIWrapper": "", /* Scrolling */ "sScrollWrapper": "dataTables_scroll", "sScrollHead": "dataTables_scrollHead", "sScrollHeadInner": "dataTables_scrollHeadInner", "sScrollBody": "dataTables_scrollBody", "sScrollFoot": "dataTables_scrollFoot", "sScrollFootInner": "dataTables_scrollFootInner", /* Misc */ "sFooterTH": "" }; /* * Variable: oJUIClasses * Purpose: Storage for the various classes that DataTables uses - jQuery UI suitable * Scope: jQuery.fn.dataTableExt */ _oExt.oJUIClasses = { /* Two buttons buttons */ "sPagePrevEnabled": "fg-button ui-button ui-state-default ui-corner-left", "sPagePrevDisabled": "fg-button ui-button ui-state-default ui-corner-left ui-state-disabled", "sPageNextEnabled": "fg-button ui-button ui-state-default ui-corner-right", "sPageNextDisabled": "fg-button ui-button ui-state-default ui-corner-right ui-state-disabled", "sPageJUINext": "ui-icon ui-icon-circle-arrow-e", "sPageJUIPrev": "ui-icon ui-icon-circle-arrow-w", /* Full numbers paging buttons */ "sPageButton": "fg-button ui-button ui-state-default", "sPageButtonActive": "fg-button ui-button ui-state-default ui-state-disabled", "sPageButtonStaticDisabled": "fg-button ui-button ui-state-default ui-state-disabled", "sPageFirst": "first ui-corner-tl ui-corner-bl", "sPagePrevious": "previous", "sPageNext": "next", "sPageLast": "last ui-corner-tr ui-corner-br", /* Stripping classes */ "sStripOdd": "odd", "sStripEven": "even", /* Empty row */ "sRowEmpty": "dataTables_empty", /* Features */ "sWrapper": "dataTables_wrapper", "sFilter": "dataTables_filter", "sInfo": "dataTables_info", "sPaging": "dataTables_paginate fg-buttonset ui-buttonset fg-buttonset-multi "+ "ui-buttonset-multi paging_", /* Note that the type is postfixed */ "sLength": "dataTables_length", "sProcessing": "dataTables_processing", /* Sorting */ "sSortAsc": "ui-state-default", "sSortDesc": "ui-state-default", "sSortable": "ui-state-default", "sSortableAsc": "ui-state-default", "sSortableDesc": "ui-state-default", "sSortableNone": "ui-state-default", "sSortColumn": "sorting_", /* Note that an int is postfixed for the sorting order */ "sSortJUIAsc": "css_right ui-icon ui-icon-triangle-1-n", "sSortJUIDesc": "css_right ui-icon ui-icon-triangle-1-s", "sSortJUI": "css_right ui-icon ui-icon-carat-2-n-s", "sSortJUIAscAllowed": "css_right ui-icon ui-icon-carat-1-n", "sSortJUIDescAllowed": "css_right ui-icon ui-icon-carat-1-s", "sSortJUIWrapper": "DataTables_sort_wrapper", /* Scrolling */ "sScrollWrapper": "dataTables_scroll", "sScrollHead": "dataTables_scrollHead ui-state-default", "sScrollHeadInner": "dataTables_scrollHeadInner", "sScrollBody": "dataTables_scrollBody", "sScrollFoot": "dataTables_scrollFoot ui-state-default", "sScrollFootInner": "dataTables_scrollFootInner", /* Misc */ "sFooterTH": "ui-state-default" }; /* * Variable: oPagination * Purpose: Container for the various type of pagination that dataTables supports * Scope: jQuery.fn.dataTableExt */ _oExt.oPagination = { /* * Variable: two_button * Purpose: Standard two button (forward/back) pagination * Scope: jQuery.fn.dataTableExt.oPagination */ "two_button": { /* * Function: oPagination.two_button.fnInit * Purpose: Initalise dom elements required for pagination with forward/back buttons only * Returns: - * Inputs: object:oSettings - dataTables settings object * node:nPaging - the DIV which contains this pagination control * function:fnCallbackDraw - draw function which must be called on update */ "fnInit": function ( oSettings, nPaging, fnCallbackDraw ) { var nPrevious, nNext, nPreviousInner, nNextInner; /* Store the next and previous elements in the oSettings object as they can be very * usful for automation - particularly testing */ if ( !oSettings.bJUI ) { nPrevious = document.createElement( 'div' ); nNext = document.createElement( 'div' ); } else { nPrevious = document.createElement( 'a' ); nNext = document.createElement( 'a' ); nNextInner = document.createElement('span'); nNextInner.className = oSettings.oClasses.sPageJUINext; nNext.appendChild( nNextInner ); nPreviousInner = document.createElement('span'); nPreviousInner.className = oSettings.oClasses.sPageJUIPrev; nPrevious.appendChild( nPreviousInner ); } nPrevious.className = oSettings.oClasses.sPagePrevDisabled; nNext.className = oSettings.oClasses.sPageNextDisabled; nPrevious.title = oSettings.oLanguage.oPaginate.sPrevious; nNext.title = oSettings.oLanguage.oPaginate.sNext; nPaging.appendChild( nPrevious ); nPaging.appendChild( nNext ); $(nPrevious).click( function() { if ( oSettings.oApi._fnPageChange( oSettings, "previous" ) ) { /* Only draw when the page has actually changed */ fnCallbackDraw( oSettings ); } } ); $(nNext).click( function() { if ( oSettings.oApi._fnPageChange( oSettings, "next" ) ) { fnCallbackDraw( oSettings ); } } ); /* Take the brutal approach to cancelling text selection */ $(nPrevious).bind( 'selectstart', function () { return false; } ); $(nNext).bind( 'selectstart', function () { return false; } ); /* ID the first elements only */ if ( oSettings.sTableId !== '' && typeof oSettings.aanFeatures.p == "undefined" ) { nPaging.setAttribute( 'id', oSettings.sTableId+'_paginate' ); nPrevious.setAttribute( 'id', oSettings.sTableId+'_previous' ); nNext.setAttribute( 'id', oSettings.sTableId+'_next' ); } }, /* * Function: oPagination.two_button.fnUpdate * Purpose: Update the two button pagination at the end of the draw * Returns: - * Inputs: object:oSettings - dataTables settings object * function:fnCallbackDraw - draw function to call on page change */ "fnUpdate": function ( oSettings, fnCallbackDraw ) { if ( !oSettings.aanFeatures.p ) { return; } /* Loop over each instance of the pager */ var an = oSettings.aanFeatures.p; for ( var i=0, iLen=an.length ; i= (iPages - iPageCountHalf)) { iStartButton = iPages - iPageCount + 1; iEndButton = iPages; } else { iStartButton = iCurrentPage - Math.ceil(iPageCount / 2) + 1; iEndButton = iStartButton + iPageCount - 1; } } } /* Build the dynamic list */ for ( i=iStartButton ; i<=iEndButton ; i++ ) { if ( iCurrentPage != i ) { sList += ''+i+''; } else { sList += ''+i+''; } } /* Loop over each instance of the pager */ var an = oSettings.aanFeatures.p; var anButtons, anStatic, nPaginateList; var fnClick = function() { /* Use the information in the element to jump to the required page */ var iTarget = (this.innerHTML * 1) - 1; oSettings._iDisplayStart = iTarget * oSettings._iDisplayLength; fnCallbackDraw( oSettings ); return false; }; var fnFalse = function () { return false; }; for ( i=0, iLen=an.length ; i y) ? 1 : 0)); }, "string-desc": function ( a, b ) { var x = a.toLowerCase(); var y = b.toLowerCase(); return ((x < y) ? 1 : ((x > y) ? -1 : 0)); }, /* * html sorting (ignore html tags) */ "html-asc": function ( a, b ) { var x = a.replace( /<.*?>/g, "" ).toLowerCase(); var y = b.replace( /<.*?>/g, "" ).toLowerCase(); return ((x < y) ? -1 : ((x > y) ? 1 : 0)); }, "html-desc": function ( a, b ) { var x = a.replace( /<.*?>/g, "" ).toLowerCase(); var y = b.replace( /<.*?>/g, "" ).toLowerCase(); return ((x < y) ? 1 : ((x > y) ? -1 : 0)); }, /* * date sorting */ "date-asc": function ( a, b ) { var x = Date.parse( a ); var y = Date.parse( b ); if ( isNaN(x) || x==="" ) { x = Date.parse( "01/01/1970 00:00:00" ); } if ( isNaN(y) || y==="" ) { y = Date.parse( "01/01/1970 00:00:00" ); } return x - y; }, "date-desc": function ( a, b ) { var x = Date.parse( a ); var y = Date.parse( b ); if ( isNaN(x) || x==="" ) { x = Date.parse( "01/01/1970 00:00:00" ); } if ( isNaN(y) || y==="" ) { y = Date.parse( "01/01/1970 00:00:00" ); } return y - x; }, /* * numerical sorting */ "numeric-asc": function ( a, b ) { var x = (a=="-" || a==="") ? 0 : a*1; var y = (b=="-" || b==="") ? 0 : b*1; return x - y; }, "numeric-desc": function ( a, b ) { var x = (a=="-" || a==="") ? 0 : a*1; var y = (b=="-" || b==="") ? 0 : b*1; return y - x; } }; /* * Variable: aTypes * Purpose: Container for the various type of type detection that dataTables supports * Scope: jQuery.fn.dataTableExt * Notes: The functions in this array are expected to parse a string to see if it is a data * type that it recognises. If so then the function should return the name of the type (a * corresponding sort function should be defined!), if the type is not recognised then the * function should return null such that the parser and move on to check the next type. * Note that ordering is important in this array - the functions are processed linearly, * starting at index 0. * Note that the input for these functions is always a string! It cannot be any other data * type */ _oExt.aTypes = [ /* * Function: - * Purpose: Check to see if a string is numeric * Returns: string:'numeric' or null * Inputs: string:sText - string to check */ function ( sData ) { /* Allow zero length strings as a number */ if ( sData.length === 0 ) { return 'numeric'; } var sValidFirstChars = "0123456789-"; var sValidChars = "0123456789."; var Char; var bDecimal = false; /* Check for a valid first char (no period and allow negatives) */ Char = sData.charAt(0); if (sValidFirstChars.indexOf(Char) == -1) { return null; } /* Check all the other characters are valid */ for ( var i=1 ; i') != -1 ) { return 'html'; } return null; } ]; /* * Function: fnVersionCheck * Purpose: Check a version string against this version of DataTables. Useful for plug-ins * Returns: bool:true -this version of DataTables is greater or equal to the required version * false -this version of DataTales is not suitable * Inputs: string:sVersion - the version to check against. May be in the following formats: * "a", "a.b" or "a.b.c" * Notes: This function will only check the first three parts of a version string. It is * assumed that beta and dev versions will meet the requirements. This might change in future */ _oExt.fnVersionCheck = function( sVersion ) { /* This is cheap, but very effective */ var fnZPad = function (Zpad, count) { while(Zpad.length < count) { Zpad += '0'; } return Zpad; }; var aThis = _oExt.sVersion.split('.'); var aThat = sVersion.split('.'); var sThis = '', sThat = ''; for ( var i=0, iLen=aThat.length ; i= parseInt(sThat, 10); }; /* * Variable: _oExternConfig * Purpose: Store information for DataTables to access globally about other instances * Scope: jQuery.fn.dataTableExt */ _oExt._oExternConfig = { /* int:iNextUnique - next unique number for an instance */ "iNextUnique": 0 }; /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * Section - DataTables prototype * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ /* * Function: dataTable * Purpose: DataTables information * Returns: - * Inputs: object:oInit - initalisation options for the table */ $.fn.dataTable = function( oInit ) { /* * Function: classSettings * Purpose: Settings container function for all 'class' properties which are required * by dataTables * Returns: - * Inputs: - */ function classSettings () { this.fnRecordsTotal = function () { if ( this.oFeatures.bServerSide ) { return parseInt(this._iRecordsTotal, 10); } else { return this.aiDisplayMaster.length; } }; this.fnRecordsDisplay = function () { if ( this.oFeatures.bServerSide ) { return parseInt(this._iRecordsDisplay, 10); } else { return this.aiDisplay.length; } }; this.fnDisplayEnd = function () { if ( this.oFeatures.bServerSide ) { if ( this.oFeatures.bPaginate === false || this._iDisplayLength == -1 ) { return this._iDisplayStart+this.aiDisplay.length; } else { return Math.min( this._iDisplayStart+this._iDisplayLength, this._iRecordsDisplay ); } } else { return this._iDisplayEnd; } }; /* * Variable: oInstance * Purpose: The DataTables object for this table * Scope: jQuery.dataTable.classSettings */ this.oInstance = null; /* * Variable: sInstance * Purpose: Unique idendifier for each instance of the DataTables object * Scope: jQuery.dataTable.classSettings */ this.sInstance = null; /* * Variable: oFeatures * Purpose: Indicate the enablement of key dataTable features * Scope: jQuery.dataTable.classSettings */ this.oFeatures = { "bPaginate": true, "bLengthChange": true, "bFilter": true, "bSort": true, "bInfo": true, "bAutoWidth": true, "bProcessing": false, "bSortClasses": true, "bStateSave": false, "bServerSide": false }; /* * Variable: oScroll * Purpose: Container for scrolling options * Scope: jQuery.dataTable.classSettings */ this.oScroll = { "sX": "", "sXInner": "", "sY": "", "bCollapse": false, "bInfinite": false, "iLoadGap": 100, "iBarWidth": 0 }; /* * Variable: aanFeatures * Purpose: Array referencing the nodes which are used for the features * Scope: jQuery.dataTable.classSettings * Notes: The parameters of this object match what is allowed by sDom - i.e. * 'l' - Length changing * 'f' - Filtering input * 't' - The table! * 'i' - Information * 'p' - Pagination * 'r' - pRocessing */ this.aanFeatures = []; /* * Variable: oLanguage * Purpose: Store the language strings used by dataTables * Scope: jQuery.dataTable.classSettings * Notes: The words in the format _VAR_ are variables which are dynamically replaced * by javascript */ this.oLanguage = { "sProcessing": "Processing...", "sLengthMenu": "Show _MENU_ entries", "sZeroRecords": "No matching records found", "sEmptyTable": "No data available in table", "sInfo": "Showing _START_ to _END_ of _TOTAL_ entries", "sInfoEmpty": "Showing 0 to 0 of 0 entries", "sInfoFiltered": "(filtered from _MAX_ total entries)", "sInfoPostFix": "", "sSearch": "Search:", "sUrl": "", "oPaginate": { "sFirst": "First", "sPrevious": "Previous", "sNext": "Next", "sLast": "Last" }, "fnInfoCallback": null }; /* * Variable: aoData * Purpose: Store data information * Scope: jQuery.dataTable.classSettings * Notes: This is an array of objects with the following parameters: * int: _iId - internal id for tracking * array: _aData - internal data - used for sorting / filtering etc * node: nTr - display node * array node: _anHidden - hidden TD nodes * string: _sRowStripe */ this.aoData = []; /* * Variable: aiDisplay * Purpose: Array of indexes which are in the current display (after filtering etc) * Scope: jQuery.dataTable.classSettings */ this.aiDisplay = []; /* * Variable: aiDisplayMaster * Purpose: Array of indexes for display - no filtering * Scope: jQuery.dataTable.classSettings */ this.aiDisplayMaster = []; /* * Variable: aoColumns * Purpose: Store information about each column that is in use * Scope: jQuery.dataTable.classSettings */ this.aoColumns = []; /* * Variable: iNextId * Purpose: Store the next unique id to be used for a new row * Scope: jQuery.dataTable.classSettings */ this.iNextId = 0; /* * Variable: asDataSearch * Purpose: Search data array for regular expression searching * Scope: jQuery.dataTable.classSettings */ this.asDataSearch = []; /* * Variable: oPreviousSearch * Purpose: Store the previous search incase we want to force a re-search * or compare the old search to a new one * Scope: jQuery.dataTable.classSettings */ this.oPreviousSearch = { "sSearch": "", "bRegex": false, "bSmart": true }; /* * Variable: aoPreSearchCols * Purpose: Store the previous search for each column * Scope: jQuery.dataTable.classSettings */ this.aoPreSearchCols = []; /* * Variable: aaSorting * Purpose: Sorting information * Scope: jQuery.dataTable.classSettings * Notes: Index 0 - column number * Index 1 - current sorting direction * Index 2 - index of asSorting for this column */ this.aaSorting = [ [0, 'asc', 0] ]; /* * Variable: aaSortingFixed * Purpose: Sorting information that is always applied * Scope: jQuery.dataTable.classSettings */ this.aaSortingFixed = null; /* * Variable: asStripClasses * Purpose: Classes to use for the striping of a table * Scope: jQuery.dataTable.classSettings */ this.asStripClasses = []; /* * Variable: asDestoryStrips * Purpose: If restoring a table - we should restore it's striping classes as well * Scope: jQuery.dataTable.classSettings */ this.asDestoryStrips = []; /* * Variable: sDestroyWidth * Purpose: If restoring a table - we should restore it's width * Scope: jQuery.dataTable.classSettings */ this.sDestroyWidth = 0; /* * Variable: fnRowCallback * Purpose: Call this function every time a row is inserted (draw) * Scope: jQuery.dataTable.classSettings */ this.fnRowCallback = null; /* * Variable: fnHeaderCallback * Purpose: Callback function for the header on each draw * Scope: jQuery.dataTable.classSettings */ this.fnHeaderCallback = null; /* * Variable: fnFooterCallback * Purpose: Callback function for the footer on each draw * Scope: jQuery.dataTable.classSettings */ this.fnFooterCallback = null; /* * Variable: aoDrawCallback * Purpose: Array of callback functions for draw callback functions * Scope: jQuery.dataTable.classSettings * Notes: Each array element is an object with the following parameters: * function:fn - function to call * string:sName - name callback (feature). useful for arranging array */ this.aoDrawCallback = []; /* * Variable: fnInitComplete * Purpose: Callback function for when the table has been initalised * Scope: jQuery.dataTable.classSettings */ this.fnInitComplete = null; /* * Variable: sTableId * Purpose: Cache the table ID for quick access * Scope: jQuery.dataTable.classSettings */ this.sTableId = ""; /* * Variable: nTable * Purpose: Cache the table node for quick access * Scope: jQuery.dataTable.classSettings */ this.nTable = null; /* * Variable: nTHead * Purpose: Permanent ref to the thead element * Scope: jQuery.dataTable.classSettings */ this.nTHead = null; /* * Variable: nTFoot * Purpose: Permanent ref to the tfoot element - if it exists * Scope: jQuery.dataTable.classSettings */ this.nTFoot = null; /* * Variable: nTBody * Purpose: Permanent ref to the tbody element * Scope: jQuery.dataTable.classSettings */ this.nTBody = null; /* * Variable: nTableWrapper * Purpose: Cache the wrapper node (contains all DataTables controlled elements) * Scope: jQuery.dataTable.classSettings */ this.nTableWrapper = null; /* * Variable: bInitialised * Purpose: Indicate if all required information has been read in * Scope: jQuery.dataTable.classSettings */ this.bInitialised = false; /* * Variable: aoOpenRows * Purpose: Information about open rows * Scope: jQuery.dataTable.classSettings * Notes: Has the parameters 'nTr' and 'nParent' */ this.aoOpenRows = []; /* * Variable: sDom * Purpose: Dictate the positioning that the created elements will take * Scope: jQuery.dataTable.classSettings * Notes: * The following options are allowed: * 'l' - Length changing * 'f' - Filtering input * 't' - The table! * 'i' - Information * 'p' - Pagination * 'r' - pRocessing * The following constants are allowed: * 'H' - jQueryUI theme "header" classes * 'F' - jQueryUI theme "footer" classes * The following syntax is expected: * '<' and '>' - div elements * '<"class" and '>' - div with a class * Examples: * '<"wrapper"flipt>', 'ip>' */ this.sDom = 'lfrtip'; /* * Variable: sPaginationType * Purpose: Note which type of sorting should be used * Scope: jQuery.dataTable.classSettings */ this.sPaginationType = "two_button"; /* * Variable: iCookieDuration * Purpose: The cookie duration (for bStateSave) in seconds - default 2 hours * Scope: jQuery.dataTable.classSettings */ this.iCookieDuration = 60 * 60 * 2; /* * Variable: sCookiePrefix * Purpose: The cookie name prefix * Scope: jQuery.dataTable.classSettings */ this.sCookiePrefix = "SpryMedia_DataTables_"; /* * Variable: fnCookieCallback * Purpose: Callback function for cookie creation * Scope: jQuery.dataTable.classSettings */ this.fnCookieCallback = null; /* * Variable: aoStateSave * Purpose: Array of callback functions for state saving * Scope: jQuery.dataTable.classSettings * Notes: Each array element is an object with the following parameters: * function:fn - function to call. Takes two parameters, oSettings and the JSON string to * save that has been thus far created. Returns a JSON string to be inserted into a * json object (i.e. '"param": [ 0, 1, 2]') * string:sName - name of callback */ this.aoStateSave = []; /* * Variable: aoStateLoad * Purpose: Array of callback functions for state loading * Scope: jQuery.dataTable.classSettings * Notes: Each array element is an object with the following parameters: * function:fn - function to call. Takes two parameters, oSettings and the object stored. * May return false to cancel state loading. * string:sName - name of callback */ this.aoStateLoad = []; /* * Variable: oLoadedState * Purpose: State that was loaded from the cookie. Useful for back reference * Scope: jQuery.dataTable.classSettings */ this.oLoadedState = null; /* * Variable: sAjaxSource * Purpose: Source url for AJAX data for the table * Scope: jQuery.dataTable.classSettings */ this.sAjaxSource = null; /* * Variable: bAjaxDataGet * Purpose: Note if draw should be blocked while getting data * Scope: jQuery.dataTable.classSettings */ this.bAjaxDataGet = true; /* * Variable: fnServerData * Purpose: Function to get the server-side data - can be overruled by the developer * Scope: jQuery.dataTable.classSettings */ this.fnServerData = function ( url, data, callback ) { $.ajax( { "url": url, "data": data, "success": callback, "dataType": "json", "cache": false, "error": function (xhr, error, thrown) { if ( error == "parsererror" ) { alert( "DataTables warning: JSON data from server could not be parsed. "+ "This is caused by a JSON formatting error." ); } } } ); }; /* * Variable: fnFormatNumber * Purpose: Format numbers for display * Scope: jQuery.dataTable.classSettings */ this.fnFormatNumber = function ( iIn ) { if ( iIn < 1000 ) { /* A small optimisation for what is likely to be the vast majority of use cases */ return iIn; } else { var s=(iIn+""), a=s.split(""), out="", iLen=s.length; for ( var i=0 ; i
    a"; var all = div.getElementsByTagName("*"), a = div.getElementsByTagName("a")[0]; // Can't get basic test support if ( !all || !all.length || !a ) { return; } jQuery.support = { // IE strips leading whitespace when .innerHTML is used leadingWhitespace: div.firstChild.nodeType === 3, // Make sure that tbody elements aren't automatically inserted // IE will insert them into empty tables tbody: !div.getElementsByTagName("tbody").length, // Make sure that link elements get serialized correctly by innerHTML // This requires a wrapper element in IE htmlSerialize: !!div.getElementsByTagName("link").length, // Get the style information from getAttribute // (IE uses .cssText insted) style: /red/.test( a.getAttribute("style") ), // Make sure that URLs aren't manipulated // (IE normalizes it by default) hrefNormalized: a.getAttribute("href") === "/a", // Make sure that element opacity exists // (IE uses filter instead) // Use a regex to work around a WebKit issue. See #5145 opacity: /^0.55$/.test( a.style.opacity ), // Verify style float existence // (IE uses styleFloat instead of cssFloat) cssFloat: !!a.style.cssFloat, // Make sure that if no value is specified for a checkbox // that it defaults to "on". // (WebKit defaults to "" instead) checkOn: div.getElementsByTagName("input")[0].value === "on", // Make sure that a selected-by-default option has a working selected property. // (WebKit defaults to false instead of true, IE too, if it's in an optgroup) optSelected: document.createElement("select").appendChild( document.createElement("option") ).selected, parentNode: div.removeChild( div.appendChild( document.createElement("div") ) ).parentNode === null, // Will be defined later deleteExpando: true, checkClone: false, scriptEval: false, noCloneEvent: true, boxModel: null }; script.type = "text/javascript"; try { script.appendChild( document.createTextNode( "window." + id + "=1;" ) ); } catch(e) {} root.insertBefore( script, root.firstChild ); // Make sure that the execution of code works by injecting a script // tag with appendChild/createTextNode // (IE doesn't support this, fails, and uses .text instead) if ( window[ id ] ) { jQuery.support.scriptEval = true; delete window[ id ]; } // Test to see if it's possible to delete an expando from an element // Fails in Internet Explorer try { delete script.test; } catch(e) { jQuery.support.deleteExpando = false; } root.removeChild( script ); if ( div.attachEvent && div.fireEvent ) { div.attachEvent("onclick", function click() { // Cloning a node shouldn't copy over any // bound event handlers (IE does this) jQuery.support.noCloneEvent = false; div.detachEvent("onclick", click); }); div.cloneNode(true).fireEvent("onclick"); } div = document.createElement("div"); div.innerHTML = ""; var fragment = document.createDocumentFragment(); fragment.appendChild( div.firstChild ); // WebKit doesn't clone checked state correctly in fragments jQuery.support.checkClone = fragment.cloneNode(true).cloneNode(true).lastChild.checked; // Figure out if the W3C box model works as expected // document.body must exist before we can do this jQuery(function() { var div = document.createElement("div"); div.style.width = div.style.paddingLeft = "1px"; document.body.appendChild( div ); jQuery.boxModel = jQuery.support.boxModel = div.offsetWidth === 2; document.body.removeChild( div ).style.display = 'none'; div = null; }); // Technique from Juriy Zaytsev // http://thinkweb2.com/projects/prototype/detecting-event-support-without-browser-sniffing/ var eventSupported = function( eventName ) { var el = document.createElement("div"); eventName = "on" + eventName; var isSupported = (eventName in el); if ( !isSupported ) { el.setAttribute(eventName, "return;"); isSupported = typeof el[eventName] === "function"; } el = null; return isSupported; }; jQuery.support.submitBubbles = eventSupported("submit"); jQuery.support.changeBubbles = eventSupported("change"); // release memory in IE root = script = div = all = a = null; })(); jQuery.props = { "for": "htmlFor", "class": "className", readonly: "readOnly", maxlength: "maxLength", cellspacing: "cellSpacing", rowspan: "rowSpan", colspan: "colSpan", tabindex: "tabIndex", usemap: "useMap", frameborder: "frameBorder" }; var expando = "jQuery" + now(), uuid = 0, windowData = {}; jQuery.extend({ cache: {}, expando:expando, // The following elements throw uncatchable exceptions if you // attempt to add expando properties to them. noData: { "embed": true, "object": true, "applet": true }, data: function( elem, name, data ) { if ( elem.nodeName && jQuery.noData[elem.nodeName.toLowerCase()] ) { return; } elem = elem == window ? windowData : elem; var id = elem[ expando ], cache = jQuery.cache, thisCache; if ( !id && typeof name === "string" && data === undefined ) { return null; } // Compute a unique ID for the element if ( !id ) { id = ++uuid; } // Avoid generating a new cache unless none exists and we // want to manipulate it. if ( typeof name === "object" ) { elem[ expando ] = id; thisCache = cache[ id ] = jQuery.extend(true, {}, name); } else if ( !cache[ id ] ) { elem[ expando ] = id; cache[ id ] = {}; } thisCache = cache[ id ]; // Prevent overriding the named cache with undefined values if ( data !== undefined ) { thisCache[ name ] = data; } return typeof name === "string" ? thisCache[ name ] : thisCache; }, removeData: function( elem, name ) { if ( elem.nodeName && jQuery.noData[elem.nodeName.toLowerCase()] ) { return; } elem = elem == window ? windowData : elem; var id = elem[ expando ], cache = jQuery.cache, thisCache = cache[ id ]; // If we want to remove a specific section of the element's data if ( name ) { if ( thisCache ) { // Remove the section of cache data delete thisCache[ name ]; // If we've removed all the data, remove the element's cache if ( jQuery.isEmptyObject(thisCache) ) { jQuery.removeData( elem ); } } // Otherwise, we want to remove all of the element's data } else { if ( jQuery.support.deleteExpando ) { delete elem[ jQuery.expando ]; } else if ( elem.removeAttribute ) { elem.removeAttribute( jQuery.expando ); } // Completely remove the data cache delete cache[ id ]; } } }); jQuery.fn.extend({ data: function( key, value ) { if ( typeof key === "undefined" && this.length ) { return jQuery.data( this[0] ); } else if ( typeof key === "object" ) { return this.each(function() { jQuery.data( this, key ); }); } var parts = key.split("."); parts[1] = parts[1] ? "." + parts[1] : ""; if ( value === undefined ) { var data = this.triggerHandler("getData" + parts[1] + "!", [parts[0]]); if ( data === undefined && this.length ) { data = jQuery.data( this[0], key ); } return data === undefined && parts[1] ? this.data( parts[0] ) : data; } else { return this.trigger("setData" + parts[1] + "!", [parts[0], value]).each(function() { jQuery.data( this, key, value ); }); } }, removeData: function( key ) { return this.each(function() { jQuery.removeData( this, key ); }); } }); jQuery.extend({ queue: function( elem, type, data ) { if ( !elem ) { return; } type = (type || "fx") + "queue"; var q = jQuery.data( elem, type ); // Speed up dequeue by getting out quickly if this is just a lookup if ( !data ) { return q || []; } if ( !q || jQuery.isArray(data) ) { q = jQuery.data( elem, type, jQuery.makeArray(data) ); } else { q.push( data ); } return q; }, dequeue: function( elem, type ) { type = type || "fx"; var queue = jQuery.queue( elem, type ), fn = queue.shift(); // If the fx queue is dequeued, always remove the progress sentinel if ( fn === "inprogress" ) { fn = queue.shift(); } if ( fn ) { // Add a progress sentinel to prevent the fx queue from being // automatically dequeued if ( type === "fx" ) { queue.unshift("inprogress"); } fn.call(elem, function() { jQuery.dequeue(elem, type); }); } } }); jQuery.fn.extend({ queue: function( type, data ) { if ( typeof type !== "string" ) { data = type; type = "fx"; } if ( data === undefined ) { return jQuery.queue( this[0], type ); } return this.each(function( i, elem ) { var queue = jQuery.queue( this, type, data ); if ( type === "fx" && queue[0] !== "inprogress" ) { jQuery.dequeue( this, type ); } }); }, dequeue: function( type ) { return this.each(function() { jQuery.dequeue( this, type ); }); }, // Based off of the plugin by Clint Helfers, with permission. // http://blindsignals.com/index.php/2009/07/jquery-delay/ delay: function( time, type ) { time = jQuery.fx ? jQuery.fx.speeds[time] || time : time; type = type || "fx"; return this.queue( type, function() { var elem = this; setTimeout(function() { jQuery.dequeue( elem, type ); }, time ); }); }, clearQueue: function( type ) { return this.queue( type || "fx", [] ); } }); var rclass = /[\n\t]/g, rspace = /\s+/, rreturn = /\r/g, rspecialurl = /href|src|style/, rtype = /(button|input)/i, rfocusable = /(button|input|object|select|textarea)/i, rclickable = /^(a|area)$/i, rradiocheck = /radio|checkbox/; jQuery.fn.extend({ attr: function( name, value ) { return access( this, name, value, true, jQuery.attr ); }, removeAttr: function( name, fn ) { return this.each(function(){ jQuery.attr( this, name, "" ); if ( this.nodeType === 1 ) { this.removeAttribute( name ); } }); }, addClass: function( value ) { if ( jQuery.isFunction(value) ) { return this.each(function(i) { var self = jQuery(this); self.addClass( value.call(this, i, self.attr("class")) ); }); } if ( value && typeof value === "string" ) { var classNames = (value || "").split( rspace ); for ( var i = 0, l = this.length; i < l; i++ ) { var elem = this[i]; if ( elem.nodeType === 1 ) { if ( !elem.className ) { elem.className = value; } else { var className = " " + elem.className + " ", setClass = elem.className; for ( var c = 0, cl = classNames.length; c < cl; c++ ) { if ( className.indexOf( " " + classNames[c] + " " ) < 0 ) { setClass += " " + classNames[c]; } } elem.className = jQuery.trim( setClass ); } } } } return this; }, removeClass: function( value ) { if ( jQuery.isFunction(value) ) { return this.each(function(i) { var self = jQuery(this); self.removeClass( value.call(this, i, self.attr("class")) ); }); } if ( (value && typeof value === "string") || value === undefined ) { var classNames = (value || "").split(rspace); for ( var i = 0, l = this.length; i < l; i++ ) { var elem = this[i]; if ( elem.nodeType === 1 && elem.className ) { if ( value ) { var className = (" " + elem.className + " ").replace(rclass, " "); for ( var c = 0, cl = classNames.length; c < cl; c++ ) { className = className.replace(" " + classNames[c] + " ", " "); } elem.className = jQuery.trim( className ); } else { elem.className = ""; } } } } return this; }, toggleClass: function( value, stateVal ) { var type = typeof value, isBool = typeof stateVal === "boolean"; if ( jQuery.isFunction( value ) ) { return this.each(function(i) { var self = jQuery(this); self.toggleClass( value.call(this, i, self.attr("class"), stateVal), stateVal ); }); } return this.each(function() { if ( type === "string" ) { // toggle individual class names var className, i = 0, self = jQuery(this), state = stateVal, classNames = value.split( rspace ); while ( (className = classNames[ i++ ]) ) { // check each className given, space seperated list state = isBool ? state : !self.hasClass( className ); self[ state ? "addClass" : "removeClass" ]( className ); } } else if ( type === "undefined" || type === "boolean" ) { if ( this.className ) { // store className if set jQuery.data( this, "__className__", this.className ); } // toggle whole className this.className = this.className || value === false ? "" : jQuery.data( this, "__className__" ) || ""; } }); }, hasClass: function( selector ) { var className = " " + selector + " "; for ( var i = 0, l = this.length; i < l; i++ ) { if ( (" " + this[i].className + " ").replace(rclass, " ").indexOf( className ) > -1 ) { return true; } } return false; }, val: function( value ) { if ( value === undefined ) { var elem = this[0]; if ( elem ) { if ( jQuery.nodeName( elem, "option" ) ) { return (elem.attributes.value || {}).specified ? elem.value : elem.text; } // We need to handle select boxes special if ( jQuery.nodeName( elem, "select" ) ) { var index = elem.selectedIndex, values = [], options = elem.options, one = elem.type === "select-one"; // Nothing was selected if ( index < 0 ) { return null; } // Loop through all the selected options for ( var i = one ? index : 0, max = one ? index + 1 : options.length; i < max; i++ ) { var option = options[ i ]; if ( option.selected ) { // Get the specifc value for the option value = jQuery(option).val(); // We don't need an array for one selects if ( one ) { return value; } // Multi-Selects return an array values.push( value ); } } return values; } // Handle the case where in Webkit "" is returned instead of "on" if a value isn't specified if ( rradiocheck.test( elem.type ) && !jQuery.support.checkOn ) { return elem.getAttribute("value") === null ? "on" : elem.value; } // Everything else, we just grab the value return (elem.value || "").replace(rreturn, ""); } return undefined; } var isFunction = jQuery.isFunction(value); return this.each(function(i) { var self = jQuery(this), val = value; if ( this.nodeType !== 1 ) { return; } if ( isFunction ) { val = value.call(this, i, self.val()); } // Typecast each time if the value is a Function and the appended // value is therefore different each time. if ( typeof val === "number" ) { val += ""; } if ( jQuery.isArray(val) && rradiocheck.test( this.type ) ) { this.checked = jQuery.inArray( self.val(), val ) >= 0; } else if ( jQuery.nodeName( this, "select" ) ) { var values = jQuery.makeArray(val); jQuery( "option", this ).each(function() { this.selected = jQuery.inArray( jQuery(this).val(), values ) >= 0; }); if ( !values.length ) { this.selectedIndex = -1; } } else { this.value = val; } }); } }); jQuery.extend({ attrFn: { val: true, css: true, html: true, text: true, data: true, width: true, height: true, offset: true }, attr: function( elem, name, value, pass ) { // don't set attributes on text and comment nodes if ( !elem || elem.nodeType === 3 || elem.nodeType === 8 ) { return undefined; } if ( pass && name in jQuery.attrFn ) { return jQuery(elem)[name](value); } var notxml = elem.nodeType !== 1 || !jQuery.isXMLDoc( elem ), // Whether we are setting (or getting) set = value !== undefined; // Try to normalize/fix the name name = notxml && jQuery.props[ name ] || name; // Only do all the following if this is a node (faster for style) if ( elem.nodeType === 1 ) { // These attributes require special treatment var special = rspecialurl.test( name ); // Safari mis-reports the default selected property of an option // Accessing the parent's selectedIndex property fixes it if ( name === "selected" && !jQuery.support.optSelected ) { var parent = elem.parentNode; if ( parent ) { parent.selectedIndex; // Make sure that it also works with optgroups, see #5701 if ( parent.parentNode ) { parent.parentNode.selectedIndex; } } } // If applicable, access the attribute via the DOM 0 way if ( name in elem && notxml && !special ) { if ( set ) { // We can't allow the type property to be changed (since it causes problems in IE) if ( name === "type" && rtype.test( elem.nodeName ) && elem.parentNode ) { jQuery.error( "type property can't be changed" ); } elem[ name ] = value; } // browsers index elements by id/name on forms, give priority to attributes. if ( jQuery.nodeName( elem, "form" ) && elem.getAttributeNode(name) ) { return elem.getAttributeNode( name ).nodeValue; } // elem.tabIndex doesn't always return the correct value when it hasn't been explicitly set // http://fluidproject.org/blog/2008/01/09/getting-setting-and-removing-tabindex-values-with-javascript/ if ( name === "tabIndex" ) { var attributeNode = elem.getAttributeNode( "tabIndex" ); return attributeNode && attributeNode.specified ? attributeNode.value : rfocusable.test( elem.nodeName ) || rclickable.test( elem.nodeName ) && elem.href ? 0 : undefined; } return elem[ name ]; } if ( !jQuery.support.style && notxml && name === "style" ) { if ( set ) { elem.style.cssText = "" + value; } return elem.style.cssText; } if ( set ) { // convert the value to a string (all browsers do this but IE) see #1070 elem.setAttribute( name, "" + value ); } var attr = !jQuery.support.hrefNormalized && notxml && special ? // Some attributes require a special call on IE elem.getAttribute( name, 2 ) : elem.getAttribute( name ); // Non-existent attributes return null, we normalize to undefined return attr === null ? undefined : attr; } // elem is actually elem.style ... set the style // Using attr for specific style information is now deprecated. Use style instead. return jQuery.style( elem, name, value ); } }); var rnamespaces = /\.(.*)$/, fcleanup = function( nm ) { return nm.replace(/[^\w\s\.\|`]/g, function( ch ) { return "\\" + ch; }); }; /* * A number of helper functions used for managing events. * Many of the ideas behind this code originated from * Dean Edwards' addEvent library. */ jQuery.event = { // Bind an event to an element // Original by Dean Edwards add: function( elem, types, handler, data ) { if ( elem.nodeType === 3 || elem.nodeType === 8 ) { return; } // For whatever reason, IE has trouble passing the window object // around, causing it to be cloned in the process if ( elem.setInterval && ( elem !== window && !elem.frameElement ) ) { elem = window; } var handleObjIn, handleObj; if ( handler.handler ) { handleObjIn = handler; handler = handleObjIn.handler; } // Make sure that the function being executed has a unique ID if ( !handler.guid ) { handler.guid = jQuery.guid++; } // Init the element's event structure var elemData = jQuery.data( elem ); // If no elemData is found then we must be trying to bind to one of the // banned noData elements if ( !elemData ) { return; } var events = elemData.events = elemData.events || {}, eventHandle = elemData.handle, eventHandle; if ( !eventHandle ) { elemData.handle = eventHandle = function() { // Handle the second event of a trigger and when // an event is called after a page has unloaded return typeof jQuery !== "undefined" && !jQuery.event.triggered ? jQuery.event.handle.apply( eventHandle.elem, arguments ) : undefined; }; } // Add elem as a property of the handle function // This is to prevent a memory leak with non-native events in IE. eventHandle.elem = elem; // Handle multiple events separated by a space // jQuery(...).bind("mouseover mouseout", fn); types = types.split(" "); var type, i = 0, namespaces; while ( (type = types[ i++ ]) ) { handleObj = handleObjIn ? jQuery.extend({}, handleObjIn) : { handler: handler, data: data }; // Namespaced event handlers if ( type.indexOf(".") > -1 ) { namespaces = type.split("."); type = namespaces.shift(); handleObj.namespace = namespaces.slice(0).sort().join("."); } else { namespaces = []; handleObj.namespace = ""; } handleObj.type = type; handleObj.guid = handler.guid; // Get the current list of functions bound to this event var handlers = events[ type ], special = jQuery.event.special[ type ] || {}; // Init the event handler queue if ( !handlers ) { handlers = events[ type ] = []; // Check for a special event handler // Only use addEventListener/attachEvent if the special // events handler returns false if ( !special.setup || special.setup.call( elem, data, namespaces, eventHandle ) === false ) { // Bind the global event handler to the element if ( elem.addEventListener ) { elem.addEventListener( type, eventHandle, false ); } else if ( elem.attachEvent ) { elem.attachEvent( "on" + type, eventHandle ); } } } if ( special.add ) { special.add.call( elem, handleObj ); if ( !handleObj.handler.guid ) { handleObj.handler.guid = handler.guid; } } // Add the function to the element's handler list handlers.push( handleObj ); // Keep track of which events have been used, for global triggering jQuery.event.global[ type ] = true; } // Nullify elem to prevent memory leaks in IE elem = null; }, global: {}, // Detach an event or set of events from an element remove: function( elem, types, handler, pos ) { // don't do events on text and comment nodes if ( elem.nodeType === 3 || elem.nodeType === 8 ) { return; } var ret, type, fn, i = 0, all, namespaces, namespace, special, eventType, handleObj, origType, elemData = jQuery.data( elem ), events = elemData && elemData.events; if ( !elemData || !events ) { return; } // types is actually an event object here if ( types && types.type ) { handler = types.handler; types = types.type; } // Unbind all events for the element if ( !types || typeof types === "string" && types.charAt(0) === "." ) { types = types || ""; for ( type in events ) { jQuery.event.remove( elem, type + types ); } return; } // Handle multiple events separated by a space // jQuery(...).unbind("mouseover mouseout", fn); types = types.split(" "); while ( (type = types[ i++ ]) ) { origType = type; handleObj = null; all = type.indexOf(".") < 0; namespaces = []; if ( !all ) { // Namespaced event handlers namespaces = type.split("."); type = namespaces.shift(); namespace = new RegExp("(^|\\.)" + jQuery.map( namespaces.slice(0).sort(), fcleanup ).join("\\.(?:.*\\.)?") + "(\\.|$)") } eventType = events[ type ]; if ( !eventType ) { continue; } if ( !handler ) { for ( var j = 0; j < eventType.length; j++ ) { handleObj = eventType[ j ]; if ( all || namespace.test( handleObj.namespace ) ) { jQuery.event.remove( elem, origType, handleObj.handler, j ); eventType.splice( j--, 1 ); } } continue; } special = jQuery.event.special[ type ] || {}; for ( var j = pos || 0; j < eventType.length; j++ ) { handleObj = eventType[ j ]; if ( handler.guid === handleObj.guid ) { // remove the given handler for the given type if ( all || namespace.test( handleObj.namespace ) ) { if ( pos == null ) { eventType.splice( j--, 1 ); } if ( special.remove ) { special.remove.call( elem, handleObj ); } } if ( pos != null ) { break; } } } // remove generic event handler if no more handlers exist if ( eventType.length === 0 || pos != null && eventType.length === 1 ) { if ( !special.teardown || special.teardown.call( elem, namespaces ) === false ) { removeEvent( elem, type, elemData.handle ); } ret = null; delete events[ type ]; } } // Remove the expando if it's no longer used if ( jQuery.isEmptyObject( events ) ) { var handle = elemData.handle; if ( handle ) { handle.elem = null; } delete elemData.events; delete elemData.handle; if ( jQuery.isEmptyObject( elemData ) ) { jQuery.removeData( elem ); } } }, // bubbling is internal trigger: function( event, data, elem /*, bubbling */ ) { // Event object or event type var type = event.type || event, bubbling = arguments[3]; if ( !bubbling ) { event = typeof event === "object" ? // jQuery.Event object event[expando] ? event : // Object literal jQuery.extend( jQuery.Event(type), event ) : // Just the event type (string) jQuery.Event(type); if ( type.indexOf("!") >= 0 ) { event.type = type = type.slice(0, -1); event.exclusive = true; } // Handle a global trigger if ( !elem ) { // Don't bubble custom events when global (to avoid too much overhead) event.stopPropagation(); // Only trigger if we've ever bound an event for it if ( jQuery.event.global[ type ] ) { jQuery.each( jQuery.cache, function() { if ( this.events && this.events[type] ) { jQuery.event.trigger( event, data, this.handle.elem ); } }); } } // Handle triggering a single element // don't do events on text and comment nodes if ( !elem || elem.nodeType === 3 || elem.nodeType === 8 ) { return undefined; } // Clean up in case it is reused event.result = undefined; event.target = elem; // Clone the incoming data, if any data = jQuery.makeArray( data ); data.unshift( event ); } event.currentTarget = elem; // Trigger the event, it is assumed that "handle" is a function var handle = jQuery.data( elem, "handle" ); if ( handle ) { handle.apply( elem, data ); } var parent = elem.parentNode || elem.ownerDocument; // Trigger an inline bound script try { if ( !(elem && elem.nodeName && jQuery.noData[elem.nodeName.toLowerCase()]) ) { if ( elem[ "on" + type ] && elem[ "on" + type ].apply( elem, data ) === false ) { event.result = false; } } // prevent IE from throwing an error for some elements with some event types, see #3533 } catch (e) {} if ( !event.isPropagationStopped() && parent ) { jQuery.event.trigger( event, data, parent, true ); } else if ( !event.isDefaultPrevented() ) { var target = event.target, old, isClick = jQuery.nodeName(target, "a") && type === "click", special = jQuery.event.special[ type ] || {}; if ( (!special._default || special._default.call( elem, event ) === false) && !isClick && !(target && target.nodeName && jQuery.noData[target.nodeName.toLowerCase()]) ) { try { if ( target[ type ] ) { // Make sure that we don't accidentally re-trigger the onFOO events old = target[ "on" + type ]; if ( old ) { target[ "on" + type ] = null; } jQuery.event.triggered = true; target[ type ](); } // prevent IE from throwing an error for some elements with some event types, see #3533 } catch (e) {} if ( old ) { target[ "on" + type ] = old; } jQuery.event.triggered = false; } } }, handle: function( event ) { var all, handlers, namespaces, namespace, events; event = arguments[0] = jQuery.event.fix( event || window.event ); event.currentTarget = this; // Namespaced event handlers all = event.type.indexOf(".") < 0 && !event.exclusive; if ( !all ) { namespaces = event.type.split("."); event.type = namespaces.shift(); namespace = new RegExp("(^|\\.)" + namespaces.slice(0).sort().join("\\.(?:.*\\.)?") + "(\\.|$)"); } var events = jQuery.data(this, "events"), handlers = events[ event.type ]; if ( events && handlers ) { // Clone the handlers to prevent manipulation handlers = handlers.slice(0); for ( var j = 0, l = handlers.length; j < l; j++ ) { var handleObj = handlers[ j ]; // Filter the functions by class if ( all || namespace.test( handleObj.namespace ) ) { // Pass in a reference to the handler function itself // So that we can later remove it event.handler = handleObj.handler; event.data = handleObj.data; event.handleObj = handleObj; var ret = handleObj.handler.apply( this, arguments ); if ( ret !== undefined ) { event.result = ret; if ( ret === false ) { event.preventDefault(); event.stopPropagation(); } } if ( event.isImmediatePropagationStopped() ) { break; } } } } return event.result; }, props: "altKey attrChange attrName bubbles button cancelable charCode clientX clientY ctrlKey currentTarget data detail eventPhase fromElement handler keyCode layerX layerY metaKey newValue offsetX offsetY originalTarget pageX pageY prevValue relatedNode relatedTarget screenX screenY shiftKey srcElement target toElement view wheelDelta which".split(" "), fix: function( event ) { if ( event[ expando ] ) { return event; } // store a copy of the original event object // and "clone" to set read-only properties var originalEvent = event; event = jQuery.Event( originalEvent ); for ( var i = this.props.length, prop; i; ) { prop = this.props[ --i ]; event[ prop ] = originalEvent[ prop ]; } // Fix target property, if necessary if ( !event.target ) { event.target = event.srcElement || document; // Fixes #1925 where srcElement might not be defined either } // check if target is a textnode (safari) if ( event.target.nodeType === 3 ) { event.target = event.target.parentNode; } // Add relatedTarget, if necessary if ( !event.relatedTarget && event.fromElement ) { event.relatedTarget = event.fromElement === event.target ? event.toElement : event.fromElement; } // Calculate pageX/Y if missing and clientX/Y available if ( event.pageX == null && event.clientX != null ) { var doc = document.documentElement, body = document.body; event.pageX = event.clientX + (doc && doc.scrollLeft || body && body.scrollLeft || 0) - (doc && doc.clientLeft || body && body.clientLeft || 0); event.pageY = event.clientY + (doc && doc.scrollTop || body && body.scrollTop || 0) - (doc && doc.clientTop || body && body.clientTop || 0); } // Add which for key events if ( !event.which && ((event.charCode || event.charCode === 0) ? event.charCode : event.keyCode) ) { event.which = event.charCode || event.keyCode; } // Add metaKey to non-Mac browsers (use ctrl for PC's and Meta for Macs) if ( !event.metaKey && event.ctrlKey ) { event.metaKey = event.ctrlKey; } // Add which for click: 1 === left; 2 === middle; 3 === right // Note: button is not normalized, so don't use it if ( !event.which && event.button !== undefined ) { event.which = (event.button & 1 ? 1 : ( event.button & 2 ? 3 : ( event.button & 4 ? 2 : 0 ) )); } return event; }, // Deprecated, use jQuery.guid instead guid: 1E8, // Deprecated, use jQuery.proxy instead proxy: jQuery.proxy, special: { ready: { // Make sure the ready event is setup setup: jQuery.bindReady, teardown: jQuery.noop }, live: { add: function( handleObj ) { jQuery.event.add( this, handleObj.origType, jQuery.extend({}, handleObj, {handler: liveHandler}) ); }, remove: function( handleObj ) { var remove = true, type = handleObj.origType.replace(rnamespaces, ""); jQuery.each( jQuery.data(this, "events").live || [], function() { if ( type === this.origType.replace(rnamespaces, "") ) { remove = false; return false; } }); if ( remove ) { jQuery.event.remove( this, handleObj.origType, liveHandler ); } } }, beforeunload: { setup: function( data, namespaces, eventHandle ) { // We only want to do this special case on windows if ( this.setInterval ) { this.onbeforeunload = eventHandle; } return false; }, teardown: function( namespaces, eventHandle ) { if ( this.onbeforeunload === eventHandle ) { this.onbeforeunload = null; } } } } }; var removeEvent = document.removeEventListener ? function( elem, type, handle ) { elem.removeEventListener( type, handle, false ); } : function( elem, type, handle ) { elem.detachEvent( "on" + type, handle ); }; jQuery.Event = function( src ) { // Allow instantiation without the 'new' keyword if ( !this.preventDefault ) { return new jQuery.Event( src ); } // Event object if ( src && src.type ) { this.originalEvent = src; this.type = src.type; // Event type } else { this.type = src; } // timeStamp is buggy for some events on Firefox(#3843) // So we won't rely on the native value this.timeStamp = now(); // Mark it as fixed this[ expando ] = true; }; function returnFalse() { return false; } function returnTrue() { return true; } // jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding // http://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html jQuery.Event.prototype = { preventDefault: function() { this.isDefaultPrevented = returnTrue; var e = this.originalEvent; if ( !e ) { return; } // if preventDefault exists run it on the original event if ( e.preventDefault ) { e.preventDefault(); } // otherwise set the returnValue property of the original event to false (IE) e.returnValue = false; }, stopPropagation: function() { this.isPropagationStopped = returnTrue; var e = this.originalEvent; if ( !e ) { return; } // if stopPropagation exists run it on the original event if ( e.stopPropagation ) { e.stopPropagation(); } // otherwise set the cancelBubble property of the original event to true (IE) e.cancelBubble = true; }, stopImmediatePropagation: function() { this.isImmediatePropagationStopped = returnTrue; this.stopPropagation(); }, isDefaultPrevented: returnFalse, isPropagationStopped: returnFalse, isImmediatePropagationStopped: returnFalse }; // Checks if an event happened on an element within another element // Used in jQuery.event.special.mouseenter and mouseleave handlers var withinElement = function( event ) { // Check if mouse(over|out) are still within the same parent element var parent = event.relatedTarget; // Firefox sometimes assigns relatedTarget a XUL element // which we cannot access the parentNode property of try { // Traverse up the tree while ( parent && parent !== this ) { parent = parent.parentNode; } if ( parent !== this ) { // set the correct event type event.type = event.data; // handle event if we actually just moused on to a non sub-element jQuery.event.handle.apply( this, arguments ); } // assuming we've left the element since we most likely mousedover a xul element } catch(e) { } }, // In case of event delegation, we only need to rename the event.type, // liveHandler will take care of the rest. delegate = function( event ) { event.type = event.data; jQuery.event.handle.apply( this, arguments ); }; // Create mouseenter and mouseleave events jQuery.each({ mouseenter: "mouseover", mouseleave: "mouseout" }, function( orig, fix ) { jQuery.event.special[ orig ] = { setup: function( data ) { jQuery.event.add( this, fix, data && data.selector ? delegate : withinElement, orig ); }, teardown: function( data ) { jQuery.event.remove( this, fix, data && data.selector ? delegate : withinElement ); } }; }); // submit delegation if ( !jQuery.support.submitBubbles ) { jQuery.event.special.submit = { setup: function( data, namespaces ) { if ( this.nodeName.toLowerCase() !== "form" ) { jQuery.event.add(this, "click.specialSubmit", function( e ) { var elem = e.target, type = elem.type; if ( (type === "submit" || type === "image") && jQuery( elem ).closest("form").length ) { return trigger( "submit", this, arguments ); } }); jQuery.event.add(this, "keypress.specialSubmit", function( e ) { var elem = e.target, type = elem.type; if ( (type === "text" || type === "password") && jQuery( elem ).closest("form").length && e.keyCode === 13 ) { return trigger( "submit", this, arguments ); } }); } else { return false; } }, teardown: function( namespaces ) { jQuery.event.remove( this, ".specialSubmit" ); } }; } // change delegation, happens here so we have bind. if ( !jQuery.support.changeBubbles ) { var formElems = /textarea|input|select/i, changeFilters, getVal = function( elem ) { var type = elem.type, val = elem.value; if ( type === "radio" || type === "checkbox" ) { val = elem.checked; } else if ( type === "select-multiple" ) { val = elem.selectedIndex > -1 ? jQuery.map( elem.options, function( elem ) { return elem.selected; }).join("-") : ""; } else if ( elem.nodeName.toLowerCase() === "select" ) { val = elem.selectedIndex; } return val; }, testChange = function testChange( e ) { var elem = e.target, data, val; if ( !formElems.test( elem.nodeName ) || elem.readOnly ) { return; } data = jQuery.data( elem, "_change_data" ); val = getVal(elem); // the current data will be also retrieved by beforeactivate if ( e.type !== "focusout" || elem.type !== "radio" ) { jQuery.data( elem, "_change_data", val ); } if ( data === undefined || val === data ) { return; } if ( data != null || val ) { e.type = "change"; return jQuery.event.trigger( e, arguments[1], elem ); } }; jQuery.event.special.change = { filters: { focusout: testChange, click: function( e ) { var elem = e.target, type = elem.type; if ( type === "radio" || type === "checkbox" || elem.nodeName.toLowerCase() === "select" ) { return testChange.call( this, e ); } }, // Change has to be called before submit // Keydown will be called before keypress, which is used in submit-event delegation keydown: function( e ) { var elem = e.target, type = elem.type; if ( (e.keyCode === 13 && elem.nodeName.toLowerCase() !== "textarea") || (e.keyCode === 32 && (type === "checkbox" || type === "radio")) || type === "select-multiple" ) { return testChange.call( this, e ); } }, // Beforeactivate happens also before the previous element is blurred // with this event you can't trigger a change event, but you can store // information/focus[in] is not needed anymore beforeactivate: function( e ) { var elem = e.target; jQuery.data( elem, "_change_data", getVal(elem) ); } }, setup: function( data, namespaces ) { if ( this.type === "file" ) { return false; } for ( var type in changeFilters ) { jQuery.event.add( this, type + ".specialChange", changeFilters[type] ); } return formElems.test( this.nodeName ); }, teardown: function( namespaces ) { jQuery.event.remove( this, ".specialChange" ); return formElems.test( this.nodeName ); } }; changeFilters = jQuery.event.special.change.filters; } function trigger( type, elem, args ) { args[0].type = type; return jQuery.event.handle.apply( elem, args ); } // Create "bubbling" focus and blur events if ( document.addEventListener ) { jQuery.each({ focus: "focusin", blur: "focusout" }, function( orig, fix ) { jQuery.event.special[ fix ] = { setup: function() { this.addEventListener( orig, handler, true ); }, teardown: function() { this.removeEventListener( orig, handler, true ); } }; function handler( e ) { e = jQuery.event.fix( e ); e.type = fix; return jQuery.event.handle.call( this, e ); } }); } jQuery.each(["bind", "one"], function( i, name ) { jQuery.fn[ name ] = function( type, data, fn ) { // Handle object literals if ( typeof type === "object" ) { for ( var key in type ) { this[ name ](key, data, type[key], fn); } return this; } if ( jQuery.isFunction( data ) ) { fn = data; data = undefined; } var handler = name === "one" ? jQuery.proxy( fn, function( event ) { jQuery( this ).unbind( event, handler ); return fn.apply( this, arguments ); }) : fn; if ( type === "unload" && name !== "one" ) { this.one( type, data, fn ); } else { for ( var i = 0, l = this.length; i < l; i++ ) { jQuery.event.add( this[i], type, handler, data ); } } return this; }; }); jQuery.fn.extend({ unbind: function( type, fn ) { // Handle object literals if ( typeof type === "object" && !type.preventDefault ) { for ( var key in type ) { this.unbind(key, type[key]); } } else { for ( var i = 0, l = this.length; i < l; i++ ) { jQuery.event.remove( this[i], type, fn ); } } return this; }, delegate: function( selector, types, data, fn ) { return this.live( types, data, fn, selector ); }, undelegate: function( selector, types, fn ) { if ( arguments.length === 0 ) { return this.unbind( "live" ); } else { return this.die( types, null, fn, selector ); } }, trigger: function( type, data ) { return this.each(function() { jQuery.event.trigger( type, data, this ); }); }, triggerHandler: function( type, data ) { if ( this[0] ) { var event = jQuery.Event( type ); event.preventDefault(); event.stopPropagation(); jQuery.event.trigger( event, data, this[0] ); return event.result; } }, toggle: function( fn ) { // Save reference to arguments for access in closure var args = arguments, i = 1; // link all the functions, so any of them can unbind this click handler while ( i < args.length ) { jQuery.proxy( fn, args[ i++ ] ); } return this.click( jQuery.proxy( fn, function( event ) { // Figure out which function to execute var lastToggle = ( jQuery.data( this, "lastToggle" + fn.guid ) || 0 ) % i; jQuery.data( this, "lastToggle" + fn.guid, lastToggle + 1 ); // Make sure that clicks stop event.preventDefault(); // and execute the function return args[ lastToggle ].apply( this, arguments ) || false; })); }, hover: function( fnOver, fnOut ) { return this.mouseenter( fnOver ).mouseleave( fnOut || fnOver ); } }); var liveMap = { focus: "focusin", blur: "focusout", mouseenter: "mouseover", mouseleave: "mouseout" }; jQuery.each(["live", "die"], function( i, name ) { jQuery.fn[ name ] = function( types, data, fn, origSelector /* Internal Use Only */ ) { var type, i = 0, match, namespaces, preType, selector = origSelector || this.selector, context = origSelector ? this : jQuery( this.context ); if ( jQuery.isFunction( data ) ) { fn = data; data = undefined; } types = (types || "").split(" "); while ( (type = types[ i++ ]) != null ) { match = rnamespaces.exec( type ); namespaces = ""; if ( match ) { namespaces = match[0]; type = type.replace( rnamespaces, "" ); } if ( type === "hover" ) { types.push( "mouseenter" + namespaces, "mouseleave" + namespaces ); continue; } preType = type; if ( type === "focus" || type === "blur" ) { types.push( liveMap[ type ] + namespaces ); type = type + namespaces; } else { type = (liveMap[ type ] || type) + namespaces; } if ( name === "live" ) { // bind live handler context.each(function(){ jQuery.event.add( this, liveConvert( type, selector ), { data: data, selector: selector, handler: fn, origType: type, origHandler: fn, preType: preType } ); }); } else { // unbind live handler context.unbind( liveConvert( type, selector ), fn ); } } return this; } }); function liveHandler( event ) { var stop, elems = [], selectors = [], args = arguments, related, match, handleObj, elem, j, i, l, data, events = jQuery.data( this, "events" ); // Make sure we avoid non-left-click bubbling in Firefox (#3861) if ( event.liveFired === this || !events || !events.live || event.button && event.type === "click" ) { return; } event.liveFired = this; var live = events.live.slice(0); for ( j = 0; j < live.length; j++ ) { handleObj = live[j]; if ( handleObj.origType.replace( rnamespaces, "" ) === event.type ) { selectors.push( handleObj.selector ); } else { live.splice( j--, 1 ); } } match = jQuery( event.target ).closest( selectors, event.currentTarget ); for ( i = 0, l = match.length; i < l; i++ ) { for ( j = 0; j < live.length; j++ ) { handleObj = live[j]; if ( match[i].selector === handleObj.selector ) { elem = match[i].elem; related = null; // Those two events require additional checking if ( handleObj.preType === "mouseenter" || handleObj.preType === "mouseleave" ) { related = jQuery( event.relatedTarget ).closest( handleObj.selector )[0]; } if ( !related || related !== elem ) { elems.push({ elem: elem, handleObj: handleObj }); } } } } for ( i = 0, l = elems.length; i < l; i++ ) { match = elems[i]; event.currentTarget = match.elem; event.data = match.handleObj.data; event.handleObj = match.handleObj; if ( match.handleObj.origHandler.apply( match.elem, args ) === false ) { stop = false; break; } } return stop; } function liveConvert( type, selector ) { return "live." + (type && type !== "*" ? type + "." : "") + selector.replace(/\./g, "`").replace(/ /g, "&"); } jQuery.each( ("blur focus focusin focusout load resize scroll unload click dblclick " + "mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave " + "change select submit keydown keypress keyup error").split(" "), function( i, name ) { // Handle event binding jQuery.fn[ name ] = function( fn ) { return fn ? this.bind( name, fn ) : this.trigger( name ); }; if ( jQuery.attrFn ) { jQuery.attrFn[ name ] = true; } }); // Prevent memory leaks in IE // Window isn't included so as not to unbind existing unload events // More info: // - http://isaacschlueter.com/2006/10/msie-memory-leaks/ if ( window.attachEvent && !window.addEventListener ) { window.attachEvent("onunload", function() { for ( var id in jQuery.cache ) { if ( jQuery.cache[ id ].handle ) { // Try/Catch is to handle iframes being unloaded, see #4280 try { jQuery.event.remove( jQuery.cache[ id ].handle.elem ); } catch(e) {} } } }); } /*! * Sizzle CSS Selector Engine - v1.0 * Copyright 2009, The Dojo Foundation * Released under the MIT, BSD, and GPL Licenses. * More information: http://sizzlejs.com/ */ (function(){ var chunker = /((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^[\]]*\]|['"][^'"]*['"]|[^[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g, done = 0, toString = Object.prototype.toString, hasDuplicate = false, baseHasDuplicate = true; // Here we check if the JavaScript engine is using some sort of // optimization where it does not always call our comparision // function. If that is the case, discard the hasDuplicate value. // Thus far that includes Google Chrome. [0, 0].sort(function(){ baseHasDuplicate = false; return 0; }); var Sizzle = function(selector, context, results, seed) { results = results || []; var origContext = context = context || document; if ( context.nodeType !== 1 && context.nodeType !== 9 ) { return []; } if ( !selector || typeof selector !== "string" ) { return results; } var parts = [], m, set, checkSet, extra, prune = true, contextXML = isXML(context), soFar = selector; // Reset the position of the chunker regexp (start from head) while ( (chunker.exec(""), m = chunker.exec(soFar)) !== null ) { soFar = m[3]; parts.push( m[1] ); if ( m[2] ) { extra = m[3]; break; } } if ( parts.length > 1 && origPOS.exec( selector ) ) { if ( parts.length === 2 && Expr.relative[ parts[0] ] ) { set = posProcess( parts[0] + parts[1], context ); } else { set = Expr.relative[ parts[0] ] ? [ context ] : Sizzle( parts.shift(), context ); while ( parts.length ) { selector = parts.shift(); if ( Expr.relative[ selector ] ) { selector += parts.shift(); } set = posProcess( selector, set ); } } } else { // Take a shortcut and set the context if the root selector is an ID // (but not if it'll be faster if the inner selector is an ID) if ( !seed && parts.length > 1 && context.nodeType === 9 && !contextXML && Expr.match.ID.test(parts[0]) && !Expr.match.ID.test(parts[parts.length - 1]) ) { var ret = Sizzle.find( parts.shift(), context, contextXML ); context = ret.expr ? Sizzle.filter( ret.expr, ret.set )[0] : ret.set[0]; } if ( context ) { var ret = seed ? { expr: parts.pop(), set: makeArray(seed) } : Sizzle.find( parts.pop(), parts.length === 1 && (parts[0] === "~" || parts[0] === "+") && context.parentNode ? context.parentNode : context, contextXML ); set = ret.expr ? Sizzle.filter( ret.expr, ret.set ) : ret.set; if ( parts.length > 0 ) { checkSet = makeArray(set); } else { prune = false; } while ( parts.length ) { var cur = parts.pop(), pop = cur; if ( !Expr.relative[ cur ] ) { cur = ""; } else { pop = parts.pop(); } if ( pop == null ) { pop = context; } Expr.relative[ cur ]( checkSet, pop, contextXML ); } } else { checkSet = parts = []; } } if ( !checkSet ) { checkSet = set; } if ( !checkSet ) { Sizzle.error( cur || selector ); } if ( toString.call(checkSet) === "[object Array]" ) { if ( !prune ) { results.push.apply( results, checkSet ); } else if ( context && context.nodeType === 1 ) { for ( var i = 0; checkSet[i] != null; i++ ) { if ( checkSet[i] && (checkSet[i] === true || checkSet[i].nodeType === 1 && contains(context, checkSet[i])) ) { results.push( set[i] ); } } } else { for ( var i = 0; checkSet[i] != null; i++ ) { if ( checkSet[i] && checkSet[i].nodeType === 1 ) { results.push( set[i] ); } } } } else { makeArray( checkSet, results ); } if ( extra ) { Sizzle( extra, origContext, results, seed ); Sizzle.uniqueSort( results ); } return results; }; Sizzle.uniqueSort = function(results){ if ( sortOrder ) { hasDuplicate = baseHasDuplicate; results.sort(sortOrder); if ( hasDuplicate ) { for ( var i = 1; i < results.length; i++ ) { if ( results[i] === results[i-1] ) { results.splice(i--, 1); } } } } return results; }; Sizzle.matches = function(expr, set){ return Sizzle(expr, null, null, set); }; Sizzle.find = function(expr, context, isXML){ var set, match; if ( !expr ) { return []; } for ( var i = 0, l = Expr.order.length; i < l; i++ ) { var type = Expr.order[i], match; if ( (match = Expr.leftMatch[ type ].exec( expr )) ) { var left = match[1]; match.splice(1,1); if ( left.substr( left.length - 1 ) !== "\\" ) { match[1] = (match[1] || "").replace(/\\/g, ""); set = Expr.find[ type ]( match, context, isXML ); if ( set != null ) { expr = expr.replace( Expr.match[ type ], "" ); break; } } } } if ( !set ) { set = context.getElementsByTagName("*"); } return {set: set, expr: expr}; }; Sizzle.filter = function(expr, set, inplace, not){ var old = expr, result = [], curLoop = set, match, anyFound, isXMLFilter = set && set[0] && isXML(set[0]); while ( expr && set.length ) { for ( var type in Expr.filter ) { if ( (match = Expr.leftMatch[ type ].exec( expr )) != null && match[2] ) { var filter = Expr.filter[ type ], found, item, left = match[1]; anyFound = false; match.splice(1,1); if ( left.substr( left.length - 1 ) === "\\" ) { continue; } if ( curLoop === result ) { result = []; } if ( Expr.preFilter[ type ] ) { match = Expr.preFilter[ type ]( match, curLoop, inplace, result, not, isXMLFilter ); if ( !match ) { anyFound = found = true; } else if ( match === true ) { continue; } } if ( match ) { for ( var i = 0; (item = curLoop[i]) != null; i++ ) { if ( item ) { found = filter( item, match, i, curLoop ); var pass = not ^ !!found; if ( inplace && found != null ) { if ( pass ) { anyFound = true; } else { curLoop[i] = false; } } else if ( pass ) { result.push( item ); anyFound = true; } } } } if ( found !== undefined ) { if ( !inplace ) { curLoop = result; } expr = expr.replace( Expr.match[ type ], "" ); if ( !anyFound ) { return []; } break; } } } // Improper expression if ( expr === old ) { if ( anyFound == null ) { Sizzle.error( expr ); } else { break; } } old = expr; } return curLoop; }; Sizzle.error = function( msg ) { throw "Syntax error, unrecognized expression: " + msg; }; var Expr = Sizzle.selectors = { order: [ "ID", "NAME", "TAG" ], match: { ID: /#((?:[\w\u00c0-\uFFFF-]|\\.)+)/, CLASS: /\.((?:[\w\u00c0-\uFFFF-]|\\.)+)/, NAME: /\[name=['"]*((?:[\w\u00c0-\uFFFF-]|\\.)+)['"]*\]/, ATTR: /\[\s*((?:[\w\u00c0-\uFFFF-]|\\.)+)\s*(?:(\S?=)\s*(['"]*)(.*?)\3|)\s*\]/, TAG: /^((?:[\w\u00c0-\uFFFF\*-]|\\.)+)/, CHILD: /:(only|nth|last|first)-child(?:\((even|odd|[\dn+-]*)\))?/, POS: /:(nth|eq|gt|lt|first|last|even|odd)(?:\((\d*)\))?(?=[^-]|$)/, PSEUDO: /:((?:[\w\u00c0-\uFFFF-]|\\.)+)(?:\((['"]?)((?:\([^\)]+\)|[^\(\)]*)+)\2\))?/ }, leftMatch: {}, attrMap: { "class": "className", "for": "htmlFor" }, attrHandle: { href: function(elem){ return elem.getAttribute("href"); } }, relative: { "+": function(checkSet, part){ var isPartStr = typeof part === "string", isTag = isPartStr && !/\W/.test(part), isPartStrNotTag = isPartStr && !isTag; if ( isTag ) { part = part.toLowerCase(); } for ( var i = 0, l = checkSet.length, elem; i < l; i++ ) { if ( (elem = checkSet[i]) ) { while ( (elem = elem.previousSibling) && elem.nodeType !== 1 ) {} checkSet[i] = isPartStrNotTag || elem && elem.nodeName.toLowerCase() === part ? elem || false : elem === part; } } if ( isPartStrNotTag ) { Sizzle.filter( part, checkSet, true ); } }, ">": function(checkSet, part){ var isPartStr = typeof part === "string"; if ( isPartStr && !/\W/.test(part) ) { part = part.toLowerCase(); for ( var i = 0, l = checkSet.length; i < l; i++ ) { var elem = checkSet[i]; if ( elem ) { var parent = elem.parentNode; checkSet[i] = parent.nodeName.toLowerCase() === part ? parent : false; } } } else { for ( var i = 0, l = checkSet.length; i < l; i++ ) { var elem = checkSet[i]; if ( elem ) { checkSet[i] = isPartStr ? elem.parentNode : elem.parentNode === part; } } if ( isPartStr ) { Sizzle.filter( part, checkSet, true ); } } }, "": function(checkSet, part, isXML){ var doneName = done++, checkFn = dirCheck; if ( typeof part === "string" && !/\W/.test(part) ) { var nodeCheck = part = part.toLowerCase(); checkFn = dirNodeCheck; } checkFn("parentNode", part, doneName, checkSet, nodeCheck, isXML); }, "~": function(checkSet, part, isXML){ var doneName = done++, checkFn = dirCheck; if ( typeof part === "string" && !/\W/.test(part) ) { var nodeCheck = part = part.toLowerCase(); checkFn = dirNodeCheck; } checkFn("previousSibling", part, doneName, checkSet, nodeCheck, isXML); } }, find: { ID: function(match, context, isXML){ if ( typeof context.getElementById !== "undefined" && !isXML ) { var m = context.getElementById(match[1]); return m ? [m] : []; } }, NAME: function(match, context){ if ( typeof context.getElementsByName !== "undefined" ) { var ret = [], results = context.getElementsByName(match[1]); for ( var i = 0, l = results.length; i < l; i++ ) { if ( results[i].getAttribute("name") === match[1] ) { ret.push( results[i] ); } } return ret.length === 0 ? null : ret; } }, TAG: function(match, context){ return context.getElementsByTagName(match[1]); } }, preFilter: { CLASS: function(match, curLoop, inplace, result, not, isXML){ match = " " + match[1].replace(/\\/g, "") + " "; if ( isXML ) { return match; } for ( var i = 0, elem; (elem = curLoop[i]) != null; i++ ) { if ( elem ) { if ( not ^ (elem.className && (" " + elem.className + " ").replace(/[\t\n]/g, " ").indexOf(match) >= 0) ) { if ( !inplace ) { result.push( elem ); } } else if ( inplace ) { curLoop[i] = false; } } } return false; }, ID: function(match){ return match[1].replace(/\\/g, ""); }, TAG: function(match, curLoop){ return match[1].toLowerCase(); }, CHILD: function(match){ if ( match[1] === "nth" ) { // parse equations like 'even', 'odd', '5', '2n', '3n+2', '4n-1', '-n+6' var test = /(-?)(\d*)n((?:\+|-)?\d*)/.exec( match[2] === "even" && "2n" || match[2] === "odd" && "2n+1" || !/\D/.test( match[2] ) && "0n+" + match[2] || match[2]); // calculate the numbers (first)n+(last) including if they are negative match[2] = (test[1] + (test[2] || 1)) - 0; match[3] = test[3] - 0; } // TODO: Move to normal caching system match[0] = done++; return match; }, ATTR: function(match, curLoop, inplace, result, not, isXML){ var name = match[1].replace(/\\/g, ""); if ( !isXML && Expr.attrMap[name] ) { match[1] = Expr.attrMap[name]; } if ( match[2] === "~=" ) { match[4] = " " + match[4] + " "; } return match; }, PSEUDO: function(match, curLoop, inplace, result, not){ if ( match[1] === "not" ) { // If we're dealing with a complex expression, or a simple one if ( ( chunker.exec(match[3]) || "" ).length > 1 || /^\w/.test(match[3]) ) { match[3] = Sizzle(match[3], null, null, curLoop); } else { var ret = Sizzle.filter(match[3], curLoop, inplace, true ^ not); if ( !inplace ) { result.push.apply( result, ret ); } return false; } } else if ( Expr.match.POS.test( match[0] ) || Expr.match.CHILD.test( match[0] ) ) { return true; } return match; }, POS: function(match){ match.unshift( true ); return match; } }, filters: { enabled: function(elem){ return elem.disabled === false && elem.type !== "hidden"; }, disabled: function(elem){ return elem.disabled === true; }, checked: function(elem){ return elem.checked === true; }, selected: function(elem){ // Accessing this property makes selected-by-default // options in Safari work properly elem.parentNode.selectedIndex; return elem.selected === true; }, parent: function(elem){ return !!elem.firstChild; }, empty: function(elem){ return !elem.firstChild; }, has: function(elem, i, match){ return !!Sizzle( match[3], elem ).length; }, header: function(elem){ return /h\d/i.test( elem.nodeName ); }, text: function(elem){ return "text" === elem.type; }, radio: function(elem){ return "radio" === elem.type; }, checkbox: function(elem){ return "checkbox" === elem.type; }, file: function(elem){ return "file" === elem.type; }, password: function(elem){ return "password" === elem.type; }, submit: function(elem){ return "submit" === elem.type; }, image: function(elem){ return "image" === elem.type; }, reset: function(elem){ return "reset" === elem.type; }, button: function(elem){ return "button" === elem.type || elem.nodeName.toLowerCase() === "button"; }, input: function(elem){ return /input|select|textarea|button/i.test(elem.nodeName); } }, setFilters: { first: function(elem, i){ return i === 0; }, last: function(elem, i, match, array){ return i === array.length - 1; }, even: function(elem, i){ return i % 2 === 0; }, odd: function(elem, i){ return i % 2 === 1; }, lt: function(elem, i, match){ return i < match[3] - 0; }, gt: function(elem, i, match){ return i > match[3] - 0; }, nth: function(elem, i, match){ return match[3] - 0 === i; }, eq: function(elem, i, match){ return match[3] - 0 === i; } }, filter: { PSEUDO: function(elem, match, i, array){ var name = match[1], filter = Expr.filters[ name ]; if ( filter ) { return filter( elem, i, match, array ); } else if ( name === "contains" ) { return (elem.textContent || elem.innerText || getText([ elem ]) || "").indexOf(match[3]) >= 0; } else if ( name === "not" ) { var not = match[3]; for ( var i = 0, l = not.length; i < l; i++ ) { if ( not[i] === elem ) { return false; } } return true; } else { Sizzle.error( "Syntax error, unrecognized expression: " + name ); } }, CHILD: function(elem, match){ var type = match[1], node = elem; switch (type) { case 'only': case 'first': while ( (node = node.previousSibling) ) { if ( node.nodeType === 1 ) { return false; } } if ( type === "first" ) { return true; } node = elem; case 'last': while ( (node = node.nextSibling) ) { if ( node.nodeType === 1 ) { return false; } } return true; case 'nth': var first = match[2], last = match[3]; if ( first === 1 && last === 0 ) { return true; } var doneName = match[0], parent = elem.parentNode; if ( parent && (parent.sizcache !== doneName || !elem.nodeIndex) ) { var count = 0; for ( node = parent.firstChild; node; node = node.nextSibling ) { if ( node.nodeType === 1 ) { node.nodeIndex = ++count; } } parent.sizcache = doneName; } var diff = elem.nodeIndex - last; if ( first === 0 ) { return diff === 0; } else { return ( diff % first === 0 && diff / first >= 0 ); } } }, ID: function(elem, match){ return elem.nodeType === 1 && elem.getAttribute("id") === match; }, TAG: function(elem, match){ return (match === "*" && elem.nodeType === 1) || elem.nodeName.toLowerCase() === match; }, CLASS: function(elem, match){ return (" " + (elem.className || elem.getAttribute("class")) + " ") .indexOf( match ) > -1; }, ATTR: function(elem, match){ var name = match[1], result = Expr.attrHandle[ name ] ? Expr.attrHandle[ name ]( elem ) : elem[ name ] != null ? elem[ name ] : elem.getAttribute( name ), value = result + "", type = match[2], check = match[4]; return result == null ? type === "!=" : type === "=" ? value === check : type === "*=" ? value.indexOf(check) >= 0 : type === "~=" ? (" " + value + " ").indexOf(check) >= 0 : !check ? value && result !== false : type === "!=" ? value !== check : type === "^=" ? value.indexOf(check) === 0 : type === "$=" ? value.substr(value.length - check.length) === check : type === "|=" ? value === check || value.substr(0, check.length + 1) === check + "-" : false; }, POS: function(elem, match, i, array){ var name = match[2], filter = Expr.setFilters[ name ]; if ( filter ) { return filter( elem, i, match, array ); } } } }; var origPOS = Expr.match.POS; for ( var type in Expr.match ) { Expr.match[ type ] = new RegExp( Expr.match[ type ].source + /(?![^\[]*\])(?![^\(]*\))/.source ); Expr.leftMatch[ type ] = new RegExp( /(^(?:.|\r|\n)*?)/.source + Expr.match[ type ].source.replace(/\\(\d+)/g, function(all, num){ return "\\" + (num - 0 + 1); })); } var makeArray = function(array, results) { array = Array.prototype.slice.call( array, 0 ); if ( results ) { results.push.apply( results, array ); return results; } return array; }; // Perform a simple check to determine if the browser is capable of // converting a NodeList to an array using builtin methods. // Also verifies that the returned array holds DOM nodes // (which is not the case in the Blackberry browser) try { Array.prototype.slice.call( document.documentElement.childNodes, 0 )[0].nodeType; // Provide a fallback method if it does not work } catch(e){ makeArray = function(array, results) { var ret = results || []; if ( toString.call(array) === "[object Array]" ) { Array.prototype.push.apply( ret, array ); } else { if ( typeof array.length === "number" ) { for ( var i = 0, l = array.length; i < l; i++ ) { ret.push( array[i] ); } } else { for ( var i = 0; array[i]; i++ ) { ret.push( array[i] ); } } } return ret; }; } var sortOrder; if ( document.documentElement.compareDocumentPosition ) { sortOrder = function( a, b ) { if ( !a.compareDocumentPosition || !b.compareDocumentPosition ) { if ( a == b ) { hasDuplicate = true; } return a.compareDocumentPosition ? -1 : 1; } var ret = a.compareDocumentPosition(b) & 4 ? -1 : a === b ? 0 : 1; if ( ret === 0 ) { hasDuplicate = true; } return ret; }; } else if ( "sourceIndex" in document.documentElement ) { sortOrder = function( a, b ) { if ( !a.sourceIndex || !b.sourceIndex ) { if ( a == b ) { hasDuplicate = true; } return a.sourceIndex ? -1 : 1; } var ret = a.sourceIndex - b.sourceIndex; if ( ret === 0 ) { hasDuplicate = true; } return ret; }; } else if ( document.createRange ) { sortOrder = function( a, b ) { if ( !a.ownerDocument || !b.ownerDocument ) { if ( a == b ) { hasDuplicate = true; } return a.ownerDocument ? -1 : 1; } var aRange = a.ownerDocument.createRange(), bRange = b.ownerDocument.createRange(); aRange.setStart(a, 0); aRange.setEnd(a, 0); bRange.setStart(b, 0); bRange.setEnd(b, 0); var ret = aRange.compareBoundaryPoints(Range.START_TO_END, bRange); if ( ret === 0 ) { hasDuplicate = true; } return ret; }; } // Utility function for retreiving the text value of an array of DOM nodes function getText( elems ) { var ret = "", elem; for ( var i = 0; elems[i]; i++ ) { elem = elems[i]; // Get the text from text nodes and CDATA nodes if ( elem.nodeType === 3 || elem.nodeType === 4 ) { ret += elem.nodeValue; // Traverse everything else, except comment nodes } else if ( elem.nodeType !== 8 ) { ret += getText( elem.childNodes ); } } return ret; } // Check to see if the browser returns elements by name when // querying by getElementById (and provide a workaround) (function(){ // We're going to inject a fake input element with a specified name var form = document.createElement("div"), id = "script" + (new Date).getTime(); form.innerHTML = ""; // Inject it into the root element, check its status, and remove it quickly var root = document.documentElement; root.insertBefore( form, root.firstChild ); // The workaround has to do additional checks after a getElementById // Which slows things down for other browsers (hence the branching) if ( document.getElementById( id ) ) { Expr.find.ID = function(match, context, isXML){ if ( typeof context.getElementById !== "undefined" && !isXML ) { var m = context.getElementById(match[1]); return m ? m.id === match[1] || typeof m.getAttributeNode !== "undefined" && m.getAttributeNode("id").nodeValue === match[1] ? [m] : undefined : []; } }; Expr.filter.ID = function(elem, match){ var node = typeof elem.getAttributeNode !== "undefined" && elem.getAttributeNode("id"); return elem.nodeType === 1 && node && node.nodeValue === match; }; } root.removeChild( form ); root = form = null; // release memory in IE })(); (function(){ // Check to see if the browser returns only elements // when doing getElementsByTagName("*") // Create a fake element var div = document.createElement("div"); div.appendChild( document.createComment("") ); // Make sure no comments are found if ( div.getElementsByTagName("*").length > 0 ) { Expr.find.TAG = function(match, context){ var results = context.getElementsByTagName(match[1]); // Filter out possible comments if ( match[1] === "*" ) { var tmp = []; for ( var i = 0; results[i]; i++ ) { if ( results[i].nodeType === 1 ) { tmp.push( results[i] ); } } results = tmp; } return results; }; } // Check to see if an attribute returns normalized href attributes div.innerHTML = ""; if ( div.firstChild && typeof div.firstChild.getAttribute !== "undefined" && div.firstChild.getAttribute("href") !== "#" ) { Expr.attrHandle.href = function(elem){ return elem.getAttribute("href", 2); }; } div = null; // release memory in IE })(); if ( document.querySelectorAll ) { (function(){ var oldSizzle = Sizzle, div = document.createElement("div"); div.innerHTML = "

    "; // Safari can't handle uppercase or unicode characters when // in quirks mode. if ( div.querySelectorAll && div.querySelectorAll(".TEST").length === 0 ) { return; } Sizzle = function(query, context, extra, seed){ context = context || document; // Only use querySelectorAll on non-XML documents // (ID selectors don't work in non-HTML documents) if ( !seed && context.nodeType === 9 && !isXML(context) ) { try { return makeArray( context.querySelectorAll(query), extra ); } catch(e){} } return oldSizzle(query, context, extra, seed); }; for ( var prop in oldSizzle ) { Sizzle[ prop ] = oldSizzle[ prop ]; } div = null; // release memory in IE })(); } (function(){ var div = document.createElement("div"); div.innerHTML = "
    "; // Opera can't find a second classname (in 9.6) // Also, make sure that getElementsByClassName actually exists if ( !div.getElementsByClassName || div.getElementsByClassName("e").length === 0 ) { return; } // Safari caches class attributes, doesn't catch changes (in 3.2) div.lastChild.className = "e"; if ( div.getElementsByClassName("e").length === 1 ) { return; } Expr.order.splice(1, 0, "CLASS"); Expr.find.CLASS = function(match, context, isXML) { if ( typeof context.getElementsByClassName !== "undefined" && !isXML ) { return context.getElementsByClassName(match[1]); } }; div = null; // release memory in IE })(); function dirNodeCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) { for ( var i = 0, l = checkSet.length; i < l; i++ ) { var elem = checkSet[i]; if ( elem ) { elem = elem[dir]; var match = false; while ( elem ) { if ( elem.sizcache === doneName ) { match = checkSet[elem.sizset]; break; } if ( elem.nodeType === 1 && !isXML ){ elem.sizcache = doneName; elem.sizset = i; } if ( elem.nodeName.toLowerCase() === cur ) { match = elem; break; } elem = elem[dir]; } checkSet[i] = match; } } } function dirCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) { for ( var i = 0, l = checkSet.length; i < l; i++ ) { var elem = checkSet[i]; if ( elem ) { elem = elem[dir]; var match = false; while ( elem ) { if ( elem.sizcache === doneName ) { match = checkSet[elem.sizset]; break; } if ( elem.nodeType === 1 ) { if ( !isXML ) { elem.sizcache = doneName; elem.sizset = i; } if ( typeof cur !== "string" ) { if ( elem === cur ) { match = true; break; } } else if ( Sizzle.filter( cur, [elem] ).length > 0 ) { match = elem; break; } } elem = elem[dir]; } checkSet[i] = match; } } } var contains = document.compareDocumentPosition ? function(a, b){ return !!(a.compareDocumentPosition(b) & 16); } : function(a, b){ return a !== b && (a.contains ? a.contains(b) : true); }; var isXML = function(elem){ // documentElement is verified for cases where it doesn't yet exist // (such as loading iframes in IE - #4833) var documentElement = (elem ? elem.ownerDocument || elem : 0).documentElement; return documentElement ? documentElement.nodeName !== "HTML" : false; }; var posProcess = function(selector, context){ var tmpSet = [], later = "", match, root = context.nodeType ? [context] : context; // Position selectors must be done after the filter // And so must :not(positional) so we move all PSEUDOs to the end while ( (match = Expr.match.PSEUDO.exec( selector )) ) { later += match[0]; selector = selector.replace( Expr.match.PSEUDO, "" ); } selector = Expr.relative[selector] ? selector + "*" : selector; for ( var i = 0, l = root.length; i < l; i++ ) { Sizzle( selector, root[i], tmpSet ); } return Sizzle.filter( later, tmpSet ); }; // EXPOSE jQuery.find = Sizzle; jQuery.expr = Sizzle.selectors; jQuery.expr[":"] = jQuery.expr.filters; jQuery.unique = Sizzle.uniqueSort; jQuery.text = getText; jQuery.isXMLDoc = isXML; jQuery.contains = contains; return; window.Sizzle = Sizzle; })(); var runtil = /Until$/, rparentsprev = /^(?:parents|prevUntil|prevAll)/, // Note: This RegExp should be improved, or likely pulled from Sizzle rmultiselector = /,/, slice = Array.prototype.slice; // Implement the identical functionality for filter and not var winnow = function( elements, qualifier, keep ) { if ( jQuery.isFunction( qualifier ) ) { return jQuery.grep(elements, function( elem, i ) { return !!qualifier.call( elem, i, elem ) === keep; }); } else if ( qualifier.nodeType ) { return jQuery.grep(elements, function( elem, i ) { return (elem === qualifier) === keep; }); } else if ( typeof qualifier === "string" ) { var filtered = jQuery.grep(elements, function( elem ) { return elem.nodeType === 1; }); if ( isSimple.test( qualifier ) ) { return jQuery.filter(qualifier, filtered, !keep); } else { qualifier = jQuery.filter( qualifier, filtered ); } } return jQuery.grep(elements, function( elem, i ) { return (jQuery.inArray( elem, qualifier ) >= 0) === keep; }); }; jQuery.fn.extend({ find: function( selector ) { var ret = this.pushStack( "", "find", selector ), length = 0; for ( var i = 0, l = this.length; i < l; i++ ) { length = ret.length; jQuery.find( selector, this[i], ret ); if ( i > 0 ) { // Make sure that the results are unique for ( var n = length; n < ret.length; n++ ) { for ( var r = 0; r < length; r++ ) { if ( ret[r] === ret[n] ) { ret.splice(n--, 1); break; } } } } } return ret; }, has: function( target ) { var targets = jQuery( target ); return this.filter(function() { for ( var i = 0, l = targets.length; i < l; i++ ) { if ( jQuery.contains( this, targets[i] ) ) { return true; } } }); }, not: function( selector ) { return this.pushStack( winnow(this, selector, false), "not", selector); }, filter: function( selector ) { return this.pushStack( winnow(this, selector, true), "filter", selector ); }, is: function( selector ) { return !!selector && jQuery.filter( selector, this ).length > 0; }, closest: function( selectors, context ) { if ( jQuery.isArray( selectors ) ) { var ret = [], cur = this[0], match, matches = {}, selector; if ( cur && selectors.length ) { for ( var i = 0, l = selectors.length; i < l; i++ ) { selector = selectors[i]; if ( !matches[selector] ) { matches[selector] = jQuery.expr.match.POS.test( selector ) ? jQuery( selector, context || this.context ) : selector; } } while ( cur && cur.ownerDocument && cur !== context ) { for ( selector in matches ) { match = matches[selector]; if ( match.jquery ? match.index(cur) > -1 : jQuery(cur).is(match) ) { ret.push({ selector: selector, elem: cur }); delete matches[selector]; } } cur = cur.parentNode; } } return ret; } var pos = jQuery.expr.match.POS.test( selectors ) ? jQuery( selectors, context || this.context ) : null; return this.map(function( i, cur ) { while ( cur && cur.ownerDocument && cur !== context ) { if ( pos ? pos.index(cur) > -1 : jQuery(cur).is(selectors) ) { return cur; } cur = cur.parentNode; } return null; }); }, // Determine the position of an element within // the matched set of elements index: function( elem ) { if ( !elem || typeof elem === "string" ) { return jQuery.inArray( this[0], // If it receives a string, the selector is used // If it receives nothing, the siblings are used elem ? jQuery( elem ) : this.parent().children() ); } // Locate the position of the desired element return jQuery.inArray( // If it receives a jQuery object, the first element is used elem.jquery ? elem[0] : elem, this ); }, add: function( selector, context ) { var set = typeof selector === "string" ? jQuery( selector, context || this.context ) : jQuery.makeArray( selector ), all = jQuery.merge( this.get(), set ); return this.pushStack( isDisconnected( set[0] ) || isDisconnected( all[0] ) ? all : jQuery.unique( all ) ); }, andSelf: function() { return this.add( this.prevObject ); } }); // A painfully simple check to see if an element is disconnected // from a document (should be improved, where feasible). function isDisconnected( node ) { return !node || !node.parentNode || node.parentNode.nodeType === 11; } jQuery.each({ parent: function( elem ) { var parent = elem.parentNode; return parent && parent.nodeType !== 11 ? parent : null; }, parents: function( elem ) { return jQuery.dir( elem, "parentNode" ); }, parentsUntil: function( elem, i, until ) { return jQuery.dir( elem, "parentNode", until ); }, next: function( elem ) { return jQuery.nth( elem, 2, "nextSibling" ); }, prev: function( elem ) { return jQuery.nth( elem, 2, "previousSibling" ); }, nextAll: function( elem ) { return jQuery.dir( elem, "nextSibling" ); }, prevAll: function( elem ) { return jQuery.dir( elem, "previousSibling" ); }, nextUntil: function( elem, i, until ) { return jQuery.dir( elem, "nextSibling", until ); }, prevUntil: function( elem, i, until ) { return jQuery.dir( elem, "previousSibling", until ); }, siblings: function( elem ) { return jQuery.sibling( elem.parentNode.firstChild, elem ); }, children: function( elem ) { return jQuery.sibling( elem.firstChild ); }, contents: function( elem ) { return jQuery.nodeName( elem, "iframe" ) ? elem.contentDocument || elem.contentWindow.document : jQuery.makeArray( elem.childNodes ); } }, function( name, fn ) { jQuery.fn[ name ] = function( until, selector ) { var ret = jQuery.map( this, fn, until ); if ( !runtil.test( name ) ) { selector = until; } if ( selector && typeof selector === "string" ) { ret = jQuery.filter( selector, ret ); } ret = this.length > 1 ? jQuery.unique( ret ) : ret; if ( (this.length > 1 || rmultiselector.test( selector )) && rparentsprev.test( name ) ) { ret = ret.reverse(); } return this.pushStack( ret, name, slice.call(arguments).join(",") ); }; }); jQuery.extend({ filter: function( expr, elems, not ) { if ( not ) { expr = ":not(" + expr + ")"; } return jQuery.find.matches(expr, elems); }, dir: function( elem, dir, until ) { var matched = [], cur = elem[dir]; while ( cur && cur.nodeType !== 9 && (until === undefined || cur.nodeType !== 1 || !jQuery( cur ).is( until )) ) { if ( cur.nodeType === 1 ) { matched.push( cur ); } cur = cur[dir]; } return matched; }, nth: function( cur, result, dir, elem ) { result = result || 1; var num = 0; for ( ; cur; cur = cur[dir] ) { if ( cur.nodeType === 1 && ++num === result ) { break; } } return cur; }, sibling: function( n, elem ) { var r = []; for ( ; n; n = n.nextSibling ) { if ( n.nodeType === 1 && n !== elem ) { r.push( n ); } } return r; } }); var rinlinejQuery = / jQuery\d+="(?:\d+|null)"/g, rleadingWhitespace = /^\s+/, rxhtmlTag = /(<([\w:]+)[^>]*?)\/>/g, rselfClosing = /^(?:area|br|col|embed|hr|img|input|link|meta|param)$/i, rtagName = /<([\w:]+)/, rtbody = /"; }, wrapMap = { option: [ 1, "" ], legend: [ 1, "
    ", "
    " ], thead: [ 1, "", "
    " ], tr: [ 2, "", "
    " ], td: [ 3, "", "
    " ], col: [ 2, "", "
    " ], area: [ 1, "", "" ], _default: [ 0, "", "" ] }; wrapMap.optgroup = wrapMap.option; wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; wrapMap.th = wrapMap.td; // IE can't serialize and
    ================================================ FILE: src/test/SpecRunner.html ================================================ Scraper Tests ================================================ FILE: src/test/lib/jasmine-1.0.1/MIT.LICENSE ================================================ Copyright (c) 2008-2010 Pivotal Labs Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: src/test/lib/jasmine-1.0.1/jasmine-html.js ================================================ jasmine.TrivialReporter = function(doc) { this.document = doc || document; this.suiteDivs = {}; this.logRunningSpecs = false; }; jasmine.TrivialReporter.prototype.createDom = function(type, attrs, childrenVarArgs) { var el = document.createElement(type); for (var i = 2; i < arguments.length; i++) { var child = arguments[i]; if (typeof child === 'string') { el.appendChild(document.createTextNode(child)); } else { if (child) { el.appendChild(child); } } } for (var attr in attrs) { if (attr == "className") { el[attr] = attrs[attr]; } else { el.setAttribute(attr, attrs[attr]); } } return el; }; jasmine.TrivialReporter.prototype.reportRunnerStarting = function(runner) { var showPassed, showSkipped; this.outerDiv = this.createDom('div', { className: 'jasmine_reporter' }, this.createDom('div', { className: 'banner' }, this.createDom('div', { className: 'logo' }, this.createDom('a', { href: 'http://pivotal.github.com/jasmine/', target: "_blank" }, "Jasmine"), this.createDom('span', { className: 'version' }, runner.env.versionString())), this.createDom('div', { className: 'options' }, "Show ", showPassed = this.createDom('input', { id: "__jasmine_TrivialReporter_showPassed__", type: 'checkbox' }), this.createDom('label', { "for": "__jasmine_TrivialReporter_showPassed__" }, " passed "), showSkipped = this.createDom('input', { id: "__jasmine_TrivialReporter_showSkipped__", type: 'checkbox' }), this.createDom('label', { "for": "__jasmine_TrivialReporter_showSkipped__" }, " skipped") ) ), this.runnerDiv = this.createDom('div', { className: 'runner running' }, this.createDom('a', { className: 'run_spec', href: '?' }, "run all"), this.runnerMessageSpan = this.createDom('span', {}, "Running..."), this.finishedAtSpan = this.createDom('span', { className: 'finished-at' }, "")) ); this.document.body.appendChild(this.outerDiv); var suites = runner.suites(); for (var i = 0; i < suites.length; i++) { var suite = suites[i]; var suiteDiv = this.createDom('div', { className: 'suite' }, this.createDom('a', { className: 'run_spec', href: '?spec=' + encodeURIComponent(suite.getFullName()) }, "run"), this.createDom('a', { className: 'description', href: '?spec=' + encodeURIComponent(suite.getFullName()) }, suite.description)); this.suiteDivs[suite.id] = suiteDiv; var parentDiv = this.outerDiv; if (suite.parentSuite) { parentDiv = this.suiteDivs[suite.parentSuite.id]; } parentDiv.appendChild(suiteDiv); } this.startedAt = new Date(); var self = this; showPassed.onclick = function(evt) { if (showPassed.checked) { self.outerDiv.className += ' show-passed'; } else { self.outerDiv.className = self.outerDiv.className.replace(/ show-passed/, ''); } }; showSkipped.onclick = function(evt) { if (showSkipped.checked) { self.outerDiv.className += ' show-skipped'; } else { self.outerDiv.className = self.outerDiv.className.replace(/ show-skipped/, ''); } }; }; jasmine.TrivialReporter.prototype.reportRunnerResults = function(runner) { var results = runner.results(); var className = (results.failedCount > 0) ? "runner failed" : "runner passed"; this.runnerDiv.setAttribute("class", className); //do it twice for IE this.runnerDiv.setAttribute("className", className); var specs = runner.specs(); var specCount = 0; for (var i = 0; i < specs.length; i++) { if (this.specFilter(specs[i])) { specCount++; } } var message = "" + specCount + " spec" + (specCount == 1 ? "" : "s" ) + ", " + results.failedCount + " failure" + ((results.failedCount == 1) ? "" : "s"); message += " in " + ((new Date().getTime() - this.startedAt.getTime()) / 1000) + "s"; this.runnerMessageSpan.replaceChild(this.createDom('a', { className: 'description', href: '?'}, message), this.runnerMessageSpan.firstChild); this.finishedAtSpan.appendChild(document.createTextNode("Finished at " + new Date().toString())); }; jasmine.TrivialReporter.prototype.reportSuiteResults = function(suite) { var results = suite.results(); var status = results.passed() ? 'passed' : 'failed'; if (results.totalCount == 0) { // todo: change this to check results.skipped status = 'skipped'; } this.suiteDivs[suite.id].className += " " + status; }; jasmine.TrivialReporter.prototype.reportSpecStarting = function(spec) { if (this.logRunningSpecs) { this.log('>> Jasmine Running ' + spec.suite.description + ' ' + spec.description + '...'); } }; jasmine.TrivialReporter.prototype.reportSpecResults = function(spec) { var results = spec.results(); var status = results.passed() ? 'passed' : 'failed'; if (results.skipped) { status = 'skipped'; } var specDiv = this.createDom('div', { className: 'spec ' + status }, this.createDom('a', { className: 'run_spec', href: '?spec=' + encodeURIComponent(spec.getFullName()) }, "run"), this.createDom('a', { className: 'description', href: '?spec=' + encodeURIComponent(spec.getFullName()), title: spec.getFullName() }, spec.description)); var resultItems = results.getItems(); var messagesDiv = this.createDom('div', { className: 'messages' }); for (var i = 0; i < resultItems.length; i++) { var result = resultItems[i]; if (result.type == 'log') { messagesDiv.appendChild(this.createDom('div', {className: 'resultMessage log'}, result.toString())); } else if (result.type == 'expect' && result.passed && !result.passed()) { messagesDiv.appendChild(this.createDom('div', {className: 'resultMessage fail'}, result.message)); if (result.trace.stack) { messagesDiv.appendChild(this.createDom('div', {className: 'stackTrace'}, result.trace.stack)); } } } if (messagesDiv.childNodes.length > 0) { specDiv.appendChild(messagesDiv); } this.suiteDivs[spec.suite.id].appendChild(specDiv); }; jasmine.TrivialReporter.prototype.log = function() { var console = jasmine.getGlobal().console; if (console && console.log) { if (console.log.apply) { console.log.apply(console, arguments); } else { console.log(arguments); // ie fix: console.log.apply doesn't exist on ie } } }; jasmine.TrivialReporter.prototype.getLocation = function() { return this.document.location; }; jasmine.TrivialReporter.prototype.specFilter = function(spec) { var paramMap = {}; var params = this.getLocation().search.substring(1).split('&'); for (var i = 0; i < params.length; i++) { var p = params[i].split('='); paramMap[decodeURIComponent(p[0])] = decodeURIComponent(p[1]); } if (!paramMap["spec"]) return true; return spec.getFullName().indexOf(paramMap["spec"]) == 0; }; ================================================ FILE: src/test/lib/jasmine-1.0.1/jasmine.css ================================================ body { font-family: "Helvetica Neue Light", "Lucida Grande", "Calibri", "Arial", sans-serif; } .jasmine_reporter a:visited, .jasmine_reporter a { color: #303; } .jasmine_reporter a:hover, .jasmine_reporter a:active { color: blue; } .run_spec { float:right; padding-right: 5px; font-size: .8em; text-decoration: none; } .jasmine_reporter { margin: 0 5px; } .banner { color: #303; background-color: #fef; padding: 5px; } .logo { float: left; font-size: 1.1em; padding-left: 5px; } .logo .version { font-size: .6em; padding-left: 1em; } .runner.running { background-color: yellow; } .options { text-align: right; font-size: .8em; } .suite { border: 1px outset gray; margin: 5px 0; padding-left: 1em; } .suite .suite { margin: 5px; } .suite.passed { background-color: #dfd; } .suite.failed { background-color: #fdd; } .spec { margin: 5px; padding-left: 1em; clear: both; } .spec.failed, .spec.passed, .spec.skipped { padding-bottom: 5px; border: 1px solid gray; } .spec.failed { background-color: #fbb; border-color: red; } .spec.passed { background-color: #bfb; border-color: green; } .spec.skipped { background-color: #bbb; } .messages { border-left: 1px dashed gray; padding-left: 1em; padding-right: 1em; } .passed { background-color: #cfc; display: none; } .failed { background-color: #fbb; } .skipped { color: #777; background-color: #eee; display: none; } /*.resultMessage {*/ /*white-space: pre;*/ /*}*/ .resultMessage span.result { display: block; line-height: 2em; color: black; } .resultMessage .mismatch { color: black; } .stackTrace { white-space: pre; font-size: .8em; margin-left: 10px; max-height: 5em; overflow: auto; border: 1px inset red; padding: 1em; background: #eef; } .finished-at { padding-left: 1em; font-size: .6em; } .show-passed .passed, .show-skipped .skipped { display: block; } #jasmine_content { position:fixed; right: 100%; } .runner { border: 1px solid gray; display: block; margin: 5px 0; padding: 2px 0 2px 10px; } ================================================ FILE: src/test/lib/jasmine-1.0.1/jasmine.js ================================================ /** * Top level namespace for Jasmine, a lightweight JavaScript BDD/spec/testing framework. * * @namespace */ var jasmine = {}; /** * @private */ jasmine.unimplementedMethod_ = function() { throw new Error("unimplemented method"); }; /** * Use jasmine.undefined instead of undefined, since undefined is just * a plain old variable and may be redefined by somebody else. * * @private */ jasmine.undefined = jasmine.___undefined___; /** * Default interval in milliseconds for event loop yields (e.g. to allow network activity or to refresh the screen with the HTML-based runner). Small values here may result in slow test running. Zero means no updates until all tests have completed. * */ jasmine.DEFAULT_UPDATE_INTERVAL = 250; /** * Default timeout interval in milliseconds for waitsFor() blocks. */ jasmine.DEFAULT_TIMEOUT_INTERVAL = 5000; jasmine.getGlobal = function() { function getGlobal() { return this; } return getGlobal(); }; /** * Allows for bound functions to be compared. Internal use only. * * @ignore * @private * @param base {Object} bound 'this' for the function * @param name {Function} function to find */ jasmine.bindOriginal_ = function(base, name) { var original = base[name]; if (original.apply) { return function() { return original.apply(base, arguments); }; } else { // IE support return jasmine.getGlobal()[name]; } }; jasmine.setTimeout = jasmine.bindOriginal_(jasmine.getGlobal(), 'setTimeout'); jasmine.clearTimeout = jasmine.bindOriginal_(jasmine.getGlobal(), 'clearTimeout'); jasmine.setInterval = jasmine.bindOriginal_(jasmine.getGlobal(), 'setInterval'); jasmine.clearInterval = jasmine.bindOriginal_(jasmine.getGlobal(), 'clearInterval'); jasmine.MessageResult = function(values) { this.type = 'log'; this.values = values; this.trace = new Error(); // todo: test better }; jasmine.MessageResult.prototype.toString = function() { var text = ""; for(var i = 0; i < this.values.length; i++) { if (i > 0) text += " "; if (jasmine.isString_(this.values[i])) { text += this.values[i]; } else { text += jasmine.pp(this.values[i]); } } return text; }; jasmine.ExpectationResult = function(params) { this.type = 'expect'; this.matcherName = params.matcherName; this.passed_ = params.passed; this.expected = params.expected; this.actual = params.actual; this.message = this.passed_ ? 'Passed.' : params.message; this.trace = this.passed_ ? '' : new Error(this.message); }; jasmine.ExpectationResult.prototype.toString = function () { return this.message; }; jasmine.ExpectationResult.prototype.passed = function () { return this.passed_; }; /** * Getter for the Jasmine environment. Ensures one gets created */ jasmine.getEnv = function() { return jasmine.currentEnv_ = jasmine.currentEnv_ || new jasmine.Env(); }; /** * @ignore * @private * @param value * @returns {Boolean} */ jasmine.isArray_ = function(value) { return jasmine.isA_("Array", value); }; /** * @ignore * @private * @param value * @returns {Boolean} */ jasmine.isString_ = function(value) { return jasmine.isA_("String", value); }; /** * @ignore * @private * @param value * @returns {Boolean} */ jasmine.isNumber_ = function(value) { return jasmine.isA_("Number", value); }; /** * @ignore * @private * @param {String} typeName * @param value * @returns {Boolean} */ jasmine.isA_ = function(typeName, value) { return Object.prototype.toString.apply(value) === '[object ' + typeName + ']'; }; /** * Pretty printer for expecations. Takes any object and turns it into a human-readable string. * * @param value {Object} an object to be outputted * @returns {String} */ jasmine.pp = function(value) { var stringPrettyPrinter = new jasmine.StringPrettyPrinter(); stringPrettyPrinter.format(value); return stringPrettyPrinter.string; }; /** * Returns true if the object is a DOM Node. * * @param {Object} obj object to check * @returns {Boolean} */ jasmine.isDomNode = function(obj) { return obj['nodeType'] > 0; }; /** * Returns a matchable 'generic' object of the class type. For use in expecations of type when values don't matter. * * @example * // don't care about which function is passed in, as long as it's a function * expect(mySpy).toHaveBeenCalledWith(jasmine.any(Function)); * * @param {Class} clazz * @returns matchable object of the type clazz */ jasmine.any = function(clazz) { return new jasmine.Matchers.Any(clazz); }; /** * Jasmine Spies are test doubles that can act as stubs, spies, fakes or when used in an expecation, mocks. * * Spies should be created in test setup, before expectations. They can then be checked, using the standard Jasmine * expectation syntax. Spies can be checked if they were called or not and what the calling params were. * * A Spy has the following fields: wasCalled, callCount, mostRecentCall, and argsForCall (see docs). * * Spies are torn down at the end of every spec. * * Note: Do not call new jasmine.Spy() directly - a spy must be created using spyOn, jasmine.createSpy or jasmine.createSpyObj. * * @example * // a stub * var myStub = jasmine.createSpy('myStub'); // can be used anywhere * * // spy example * var foo = { * not: function(bool) { return !bool; } * } * * // actual foo.not will not be called, execution stops * spyOn(foo, 'not'); // foo.not spied upon, execution will continue to implementation * spyOn(foo, 'not').andCallThrough(); * * // fake example * var foo = { * not: function(bool) { return !bool; } * } * * // foo.not(val) will return val * spyOn(foo, 'not').andCallFake(function(value) {return value;}); * * // mock example * foo.not(7 == 7); * expect(foo.not).toHaveBeenCalled(); * expect(foo.not).toHaveBeenCalledWith(true); * * @constructor * @see spyOn, jasmine.createSpy, jasmine.createSpyObj * @param {String} name */ jasmine.Spy = function(name) { /** * The name of the spy, if provided. */ this.identity = name || 'unknown'; /** * Is this Object a spy? */ this.isSpy = true; /** * The actual function this spy stubs. */ this.plan = function() { }; /** * Tracking of the most recent call to the spy. * @example * var mySpy = jasmine.createSpy('foo'); * mySpy(1, 2); * mySpy.mostRecentCall.args = [1, 2]; */ this.mostRecentCall = {}; /** * Holds arguments for each call to the spy, indexed by call count * @example * var mySpy = jasmine.createSpy('foo'); * mySpy(1, 2); * mySpy(7, 8); * mySpy.mostRecentCall.args = [7, 8]; * mySpy.argsForCall[0] = [1, 2]; * mySpy.argsForCall[1] = [7, 8]; */ this.argsForCall = []; this.calls = []; }; /** * Tells a spy to call through to the actual implemenatation. * * @example * var foo = { * bar: function() { // do some stuff } * } * * // defining a spy on an existing property: foo.bar * spyOn(foo, 'bar').andCallThrough(); */ jasmine.Spy.prototype.andCallThrough = function() { this.plan = this.originalValue; return this; }; /** * For setting the return value of a spy. * * @example * // defining a spy from scratch: foo() returns 'baz' * var foo = jasmine.createSpy('spy on foo').andReturn('baz'); * * // defining a spy on an existing property: foo.bar() returns 'baz' * spyOn(foo, 'bar').andReturn('baz'); * * @param {Object} value */ jasmine.Spy.prototype.andReturn = function(value) { this.plan = function() { return value; }; return this; }; /** * For throwing an exception when a spy is called. * * @example * // defining a spy from scratch: foo() throws an exception w/ message 'ouch' * var foo = jasmine.createSpy('spy on foo').andThrow('baz'); * * // defining a spy on an existing property: foo.bar() throws an exception w/ message 'ouch' * spyOn(foo, 'bar').andThrow('baz'); * * @param {String} exceptionMsg */ jasmine.Spy.prototype.andThrow = function(exceptionMsg) { this.plan = function() { throw exceptionMsg; }; return this; }; /** * Calls an alternate implementation when a spy is called. * * @example * var baz = function() { * // do some stuff, return something * } * // defining a spy from scratch: foo() calls the function baz * var foo = jasmine.createSpy('spy on foo').andCall(baz); * * // defining a spy on an existing property: foo.bar() calls an anonymnous function * spyOn(foo, 'bar').andCall(function() { return 'baz';} ); * * @param {Function} fakeFunc */ jasmine.Spy.prototype.andCallFake = function(fakeFunc) { this.plan = fakeFunc; return this; }; /** * Resets all of a spy's the tracking variables so that it can be used again. * * @example * spyOn(foo, 'bar'); * * foo.bar(); * * expect(foo.bar.callCount).toEqual(1); * * foo.bar.reset(); * * expect(foo.bar.callCount).toEqual(0); */ jasmine.Spy.prototype.reset = function() { this.wasCalled = false; this.callCount = 0; this.argsForCall = []; this.calls = []; this.mostRecentCall = {}; }; jasmine.createSpy = function(name) { var spyObj = function() { spyObj.wasCalled = true; spyObj.callCount++; var args = jasmine.util.argsToArray(arguments); spyObj.mostRecentCall.object = this; spyObj.mostRecentCall.args = args; spyObj.argsForCall.push(args); spyObj.calls.push({object: this, args: args}); return spyObj.plan.apply(this, arguments); }; var spy = new jasmine.Spy(name); for (var prop in spy) { spyObj[prop] = spy[prop]; } spyObj.reset(); return spyObj; }; /** * Determines whether an object is a spy. * * @param {jasmine.Spy|Object} putativeSpy * @returns {Boolean} */ jasmine.isSpy = function(putativeSpy) { return putativeSpy && putativeSpy.isSpy; }; /** * Creates a more complicated spy: an Object that has every property a function that is a spy. Used for stubbing something * large in one call. * * @param {String} baseName name of spy class * @param {Array} methodNames array of names of methods to make spies */ jasmine.createSpyObj = function(baseName, methodNames) { if (!jasmine.isArray_(methodNames) || methodNames.length == 0) { throw new Error('createSpyObj requires a non-empty array of method names to create spies for'); } var obj = {}; for (var i = 0; i < methodNames.length; i++) { obj[methodNames[i]] = jasmine.createSpy(baseName + '.' + methodNames[i]); } return obj; }; /** * All parameters are pretty-printed and concatenated together, then written to the current spec's output. * * Be careful not to leave calls to jasmine.log in production code. */ jasmine.log = function() { var spec = jasmine.getEnv().currentSpec; spec.log.apply(spec, arguments); }; /** * Function that installs a spy on an existing object's method name. Used within a Spec to create a spy. * * @example * // spy example * var foo = { * not: function(bool) { return !bool; } * } * spyOn(foo, 'not'); // actual foo.not will not be called, execution stops * * @see jasmine.createSpy * @param obj * @param methodName * @returns a Jasmine spy that can be chained with all spy methods */ var spyOn = function(obj, methodName) { return jasmine.getEnv().currentSpec.spyOn(obj, methodName); }; /** * Creates a Jasmine spec that will be added to the current suite. * * // TODO: pending tests * * @example * it('should be true', function() { * expect(true).toEqual(true); * }); * * @param {String} desc description of this specification * @param {Function} func defines the preconditions and expectations of the spec */ var it = function(desc, func) { return jasmine.getEnv().it(desc, func); }; /** * Creates a disabled Jasmine spec. * * A convenience method that allows existing specs to be disabled temporarily during development. * * @param {String} desc description of this specification * @param {Function} func defines the preconditions and expectations of the spec */ var xit = function(desc, func) { return jasmine.getEnv().xit(desc, func); }; /** * Starts a chain for a Jasmine expectation. * * It is passed an Object that is the actual value and should chain to one of the many * jasmine.Matchers functions. * * @param {Object} actual Actual value to test against and expected value */ var expect = function(actual) { return jasmine.getEnv().currentSpec.expect(actual); }; /** * Defines part of a jasmine spec. Used in cominbination with waits or waitsFor in asynchrnous specs. * * @param {Function} func Function that defines part of a jasmine spec. */ var runs = function(func) { jasmine.getEnv().currentSpec.runs(func); }; /** * Waits a fixed time period before moving to the next block. * * @deprecated Use waitsFor() instead * @param {Number} timeout milliseconds to wait */ var waits = function(timeout) { jasmine.getEnv().currentSpec.waits(timeout); }; /** * Waits for the latchFunction to return true before proceeding to the next block. * * @param {Function} latchFunction * @param {String} optional_timeoutMessage * @param {Number} optional_timeout */ var waitsFor = function(latchFunction, optional_timeoutMessage, optional_timeout) { jasmine.getEnv().currentSpec.waitsFor.apply(jasmine.getEnv().currentSpec, arguments); }; /** * A function that is called before each spec in a suite. * * Used for spec setup, including validating assumptions. * * @param {Function} beforeEachFunction */ var beforeEach = function(beforeEachFunction) { jasmine.getEnv().beforeEach(beforeEachFunction); }; /** * A function that is called after each spec in a suite. * * Used for restoring any state that is hijacked during spec execution. * * @param {Function} afterEachFunction */ var afterEach = function(afterEachFunction) { jasmine.getEnv().afterEach(afterEachFunction); }; /** * Defines a suite of specifications. * * Stores the description and all defined specs in the Jasmine environment as one suite of specs. Variables declared * are accessible by calls to beforeEach, it, and afterEach. Describe blocks can be nested, allowing for specialization * of setup in some tests. * * @example * // TODO: a simple suite * * // TODO: a simple suite with a nested describe block * * @param {String} description A string, usually the class under test. * @param {Function} specDefinitions function that defines several specs. */ var describe = function(description, specDefinitions) { return jasmine.getEnv().describe(description, specDefinitions); }; /** * Disables a suite of specifications. Used to disable some suites in a file, or files, temporarily during development. * * @param {String} description A string, usually the class under test. * @param {Function} specDefinitions function that defines several specs. */ var xdescribe = function(description, specDefinitions) { return jasmine.getEnv().xdescribe(description, specDefinitions); }; // Provide the XMLHttpRequest class for IE 5.x-6.x: jasmine.XmlHttpRequest = (typeof XMLHttpRequest == "undefined") ? function() { try { return new ActiveXObject("Msxml2.XMLHTTP.6.0"); } catch(e) { } try { return new ActiveXObject("Msxml2.XMLHTTP.3.0"); } catch(e) { } try { return new ActiveXObject("Msxml2.XMLHTTP"); } catch(e) { } try { return new ActiveXObject("Microsoft.XMLHTTP"); } catch(e) { } throw new Error("This browser does not support XMLHttpRequest."); } : XMLHttpRequest; /** * @namespace */ jasmine.util = {}; /** * Declare that a child class inherit it's prototype from the parent class. * * @private * @param {Function} childClass * @param {Function} parentClass */ jasmine.util.inherit = function(childClass, parentClass) { /** * @private */ var subclass = function() { }; subclass.prototype = parentClass.prototype; childClass.prototype = new subclass; }; jasmine.util.formatException = function(e) { var lineNumber; if (e.line) { lineNumber = e.line; } else if (e.lineNumber) { lineNumber = e.lineNumber; } var file; if (e.sourceURL) { file = e.sourceURL; } else if (e.fileName) { file = e.fileName; } var message = (e.name && e.message) ? (e.name + ': ' + e.message) : e.toString(); if (file && lineNumber) { message += ' in ' + file + ' (line ' + lineNumber + ')'; } return message; }; jasmine.util.htmlEscape = function(str) { if (!str) return str; return str.replace(/&/g, '&') .replace(//g, '>'); }; jasmine.util.argsToArray = function(args) { var arrayOfArgs = []; for (var i = 0; i < args.length; i++) arrayOfArgs.push(args[i]); return arrayOfArgs; }; jasmine.util.extend = function(destination, source) { for (var property in source) destination[property] = source[property]; return destination; }; /** * Environment for Jasmine * * @constructor */ jasmine.Env = function() { this.currentSpec = null; this.currentSuite = null; this.currentRunner_ = new jasmine.Runner(this); this.reporter = new jasmine.MultiReporter(); this.updateInterval = jasmine.DEFAULT_UPDATE_INTERVAL; this.defaultTimeoutInterval = jasmine.DEFAULT_TIMEOUT_INTERVAL; this.lastUpdate = 0; this.specFilter = function() { return true; }; this.nextSpecId_ = 0; this.nextSuiteId_ = 0; this.equalityTesters_ = []; // wrap matchers this.matchersClass = function() { jasmine.Matchers.apply(this, arguments); }; jasmine.util.inherit(this.matchersClass, jasmine.Matchers); jasmine.Matchers.wrapInto_(jasmine.Matchers.prototype, this.matchersClass); }; jasmine.Env.prototype.setTimeout = jasmine.setTimeout; jasmine.Env.prototype.clearTimeout = jasmine.clearTimeout; jasmine.Env.prototype.setInterval = jasmine.setInterval; jasmine.Env.prototype.clearInterval = jasmine.clearInterval; /** * @returns an object containing jasmine version build info, if set. */ jasmine.Env.prototype.version = function () { if (jasmine.version_) { return jasmine.version_; } else { throw new Error('Version not set'); } }; /** * @returns string containing jasmine version build info, if set. */ jasmine.Env.prototype.versionString = function() { if (jasmine.version_) { var version = this.version(); return version.major + "." + version.minor + "." + version.build + " revision " + version.revision; } else { return "version unknown"; } }; /** * @returns a sequential integer starting at 0 */ jasmine.Env.prototype.nextSpecId = function () { return this.nextSpecId_++; }; /** * @returns a sequential integer starting at 0 */ jasmine.Env.prototype.nextSuiteId = function () { return this.nextSuiteId_++; }; /** * Register a reporter to receive status updates from Jasmine. * @param {jasmine.Reporter} reporter An object which will receive status updates. */ jasmine.Env.prototype.addReporter = function(reporter) { this.reporter.addReporter(reporter); }; jasmine.Env.prototype.execute = function() { this.currentRunner_.execute(); }; jasmine.Env.prototype.describe = function(description, specDefinitions) { var suite = new jasmine.Suite(this, description, specDefinitions, this.currentSuite); var parentSuite = this.currentSuite; if (parentSuite) { parentSuite.add(suite); } else { this.currentRunner_.add(suite); } this.currentSuite = suite; var declarationError = null; try { specDefinitions.call(suite); } catch(e) { declarationError = e; } this.currentSuite = parentSuite; if (declarationError) { this.it("encountered a declaration exception", function() { throw declarationError; }); } return suite; }; jasmine.Env.prototype.beforeEach = function(beforeEachFunction) { if (this.currentSuite) { this.currentSuite.beforeEach(beforeEachFunction); } else { this.currentRunner_.beforeEach(beforeEachFunction); } }; jasmine.Env.prototype.currentRunner = function () { return this.currentRunner_; }; jasmine.Env.prototype.afterEach = function(afterEachFunction) { if (this.currentSuite) { this.currentSuite.afterEach(afterEachFunction); } else { this.currentRunner_.afterEach(afterEachFunction); } }; jasmine.Env.prototype.xdescribe = function(desc, specDefinitions) { return { execute: function() { } }; }; jasmine.Env.prototype.it = function(description, func) { var spec = new jasmine.Spec(this, this.currentSuite, description); this.currentSuite.add(spec); this.currentSpec = spec; if (func) { spec.runs(func); } return spec; }; jasmine.Env.prototype.xit = function(desc, func) { return { id: this.nextSpecId(), runs: function() { } }; }; jasmine.Env.prototype.compareObjects_ = function(a, b, mismatchKeys, mismatchValues) { if (a.__Jasmine_been_here_before__ === b && b.__Jasmine_been_here_before__ === a) { return true; } a.__Jasmine_been_here_before__ = b; b.__Jasmine_been_here_before__ = a; var hasKey = function(obj, keyName) { return obj != null && obj[keyName] !== jasmine.undefined; }; for (var property in b) { if (!hasKey(a, property) && hasKey(b, property)) { mismatchKeys.push("expected has key '" + property + "', but missing from actual."); } } for (property in a) { if (!hasKey(b, property) && hasKey(a, property)) { mismatchKeys.push("expected missing key '" + property + "', but present in actual."); } } for (property in b) { if (property == '__Jasmine_been_here_before__') continue; if (!this.equals_(a[property], b[property], mismatchKeys, mismatchValues)) { mismatchValues.push("'" + property + "' was '" + (b[property] ? jasmine.util.htmlEscape(b[property].toString()) : b[property]) + "' in expected, but was '" + (a[property] ? jasmine.util.htmlEscape(a[property].toString()) : a[property]) + "' in actual."); } } if (jasmine.isArray_(a) && jasmine.isArray_(b) && a.length != b.length) { mismatchValues.push("arrays were not the same length"); } delete a.__Jasmine_been_here_before__; delete b.__Jasmine_been_here_before__; return (mismatchKeys.length == 0 && mismatchValues.length == 0); }; jasmine.Env.prototype.equals_ = function(a, b, mismatchKeys, mismatchValues) { mismatchKeys = mismatchKeys || []; mismatchValues = mismatchValues || []; for (var i = 0; i < this.equalityTesters_.length; i++) { var equalityTester = this.equalityTesters_[i]; var result = equalityTester(a, b, this, mismatchKeys, mismatchValues); if (result !== jasmine.undefined) return result; } if (a === b) return true; if (a === jasmine.undefined || a === null || b === jasmine.undefined || b === null) { return (a == jasmine.undefined && b == jasmine.undefined); } if (jasmine.isDomNode(a) && jasmine.isDomNode(b)) { return a === b; } if (a instanceof Date && b instanceof Date) { return a.getTime() == b.getTime(); } if (a instanceof jasmine.Matchers.Any) { return a.matches(b); } if (b instanceof jasmine.Matchers.Any) { return b.matches(a); } if (jasmine.isString_(a) && jasmine.isString_(b)) { return (a == b); } if (jasmine.isNumber_(a) && jasmine.isNumber_(b)) { return (a == b); } if (typeof a === "object" && typeof b === "object") { return this.compareObjects_(a, b, mismatchKeys, mismatchValues); } //Straight check return (a === b); }; jasmine.Env.prototype.contains_ = function(haystack, needle) { if (jasmine.isArray_(haystack)) { for (var i = 0; i < haystack.length; i++) { if (this.equals_(haystack[i], needle)) return true; } return false; } return haystack.indexOf(needle) >= 0; }; jasmine.Env.prototype.addEqualityTester = function(equalityTester) { this.equalityTesters_.push(equalityTester); }; /** No-op base class for Jasmine reporters. * * @constructor */ jasmine.Reporter = function() { }; //noinspection JSUnusedLocalSymbols jasmine.Reporter.prototype.reportRunnerStarting = function(runner) { }; //noinspection JSUnusedLocalSymbols jasmine.Reporter.prototype.reportRunnerResults = function(runner) { }; //noinspection JSUnusedLocalSymbols jasmine.Reporter.prototype.reportSuiteResults = function(suite) { }; //noinspection JSUnusedLocalSymbols jasmine.Reporter.prototype.reportSpecStarting = function(spec) { }; //noinspection JSUnusedLocalSymbols jasmine.Reporter.prototype.reportSpecResults = function(spec) { }; //noinspection JSUnusedLocalSymbols jasmine.Reporter.prototype.log = function(str) { }; /** * Blocks are functions with executable code that make up a spec. * * @constructor * @param {jasmine.Env} env * @param {Function} func * @param {jasmine.Spec} spec */ jasmine.Block = function(env, func, spec) { this.env = env; this.func = func; this.spec = spec; }; jasmine.Block.prototype.execute = function(onComplete) { try { this.func.apply(this.spec); } catch (e) { this.spec.fail(e); } onComplete(); }; /** JavaScript API reporter. * * @constructor */ jasmine.JsApiReporter = function() { this.started = false; this.finished = false; this.suites_ = []; this.results_ = {}; }; jasmine.JsApiReporter.prototype.reportRunnerStarting = function(runner) { this.started = true; var suites = runner.topLevelSuites(); for (var i = 0; i < suites.length; i++) { var suite = suites[i]; this.suites_.push(this.summarize_(suite)); } }; jasmine.JsApiReporter.prototype.suites = function() { return this.suites_; }; jasmine.JsApiReporter.prototype.summarize_ = function(suiteOrSpec) { var isSuite = suiteOrSpec instanceof jasmine.Suite; var summary = { id: suiteOrSpec.id, name: suiteOrSpec.description, type: isSuite ? 'suite' : 'spec', children: [] }; if (isSuite) { var children = suiteOrSpec.children(); for (var i = 0; i < children.length; i++) { summary.children.push(this.summarize_(children[i])); } } return summary; }; jasmine.JsApiReporter.prototype.results = function() { return this.results_; }; jasmine.JsApiReporter.prototype.resultsForSpec = function(specId) { return this.results_[specId]; }; //noinspection JSUnusedLocalSymbols jasmine.JsApiReporter.prototype.reportRunnerResults = function(runner) { this.finished = true; }; //noinspection JSUnusedLocalSymbols jasmine.JsApiReporter.prototype.reportSuiteResults = function(suite) { }; //noinspection JSUnusedLocalSymbols jasmine.JsApiReporter.prototype.reportSpecResults = function(spec) { this.results_[spec.id] = { messages: spec.results().getItems(), result: spec.results().failedCount > 0 ? "failed" : "passed" }; }; //noinspection JSUnusedLocalSymbols jasmine.JsApiReporter.prototype.log = function(str) { }; jasmine.JsApiReporter.prototype.resultsForSpecs = function(specIds){ var results = {}; for (var i = 0; i < specIds.length; i++) { var specId = specIds[i]; results[specId] = this.summarizeResult_(this.results_[specId]); } return results; }; jasmine.JsApiReporter.prototype.summarizeResult_ = function(result){ var summaryMessages = []; var messagesLength = result.messages.length; for (var messageIndex = 0; messageIndex < messagesLength; messageIndex++) { var resultMessage = result.messages[messageIndex]; summaryMessages.push({ text: resultMessage.type == 'log' ? resultMessage.toString() : jasmine.undefined, passed: resultMessage.passed ? resultMessage.passed() : true, type: resultMessage.type, message: resultMessage.message, trace: { stack: resultMessage.passed && !resultMessage.passed() ? resultMessage.trace.stack : jasmine.undefined } }); } return { result : result.result, messages : summaryMessages }; }; /** * @constructor * @param {jasmine.Env} env * @param actual * @param {jasmine.Spec} spec */ jasmine.Matchers = function(env, actual, spec, opt_isNot) { this.env = env; this.actual = actual; this.spec = spec; this.isNot = opt_isNot || false; this.reportWasCalled_ = false; }; // todo: @deprecated as of Jasmine 0.11, remove soon [xw] jasmine.Matchers.pp = function(str) { throw new Error("jasmine.Matchers.pp() is no longer supported, please use jasmine.pp() instead!"); }; // todo: @deprecated Deprecated as of Jasmine 0.10. Rewrite your custom matchers to return true or false. [xw] jasmine.Matchers.prototype.report = function(result, failing_message, details) { throw new Error("As of jasmine 0.11, custom matchers must be implemented differently -- please see jasmine docs"); }; jasmine.Matchers.wrapInto_ = function(prototype, matchersClass) { for (var methodName in prototype) { if (methodName == 'report') continue; var orig = prototype[methodName]; matchersClass.prototype[methodName] = jasmine.Matchers.matcherFn_(methodName, orig); } }; jasmine.Matchers.matcherFn_ = function(matcherName, matcherFunction) { return function() { var matcherArgs = jasmine.util.argsToArray(arguments); var result = matcherFunction.apply(this, arguments); if (this.isNot) { result = !result; } if (this.reportWasCalled_) return result; var message; if (!result) { if (this.message) { message = this.message.apply(this, arguments); if (jasmine.isArray_(message)) { message = message[this.isNot ? 1 : 0]; } } else { var englishyPredicate = matcherName.replace(/[A-Z]/g, function(s) { return ' ' + s.toLowerCase(); }); message = "Expected " + jasmine.pp(this.actual) + (this.isNot ? " not " : " ") + englishyPredicate; if (matcherArgs.length > 0) { for (var i = 0; i < matcherArgs.length; i++) { if (i > 0) message += ","; message += " " + jasmine.pp(matcherArgs[i]); } } message += "."; } } var expectationResult = new jasmine.ExpectationResult({ matcherName: matcherName, passed: result, expected: matcherArgs.length > 1 ? matcherArgs : matcherArgs[0], actual: this.actual, message: message }); this.spec.addMatcherResult(expectationResult); return jasmine.undefined; }; }; /** * toBe: compares the actual to the expected using === * @param expected */ jasmine.Matchers.prototype.toBe = function(expected) { return this.actual === expected; }; /** * toNotBe: compares the actual to the expected using !== * @param expected * @deprecated as of 1.0. Use not.toBe() instead. */ jasmine.Matchers.prototype.toNotBe = function(expected) { return this.actual !== expected; }; /** * toEqual: compares the actual to the expected using common sense equality. Handles Objects, Arrays, etc. * * @param expected */ jasmine.Matchers.prototype.toEqual = function(expected) { return this.env.equals_(this.actual, expected); }; /** * toNotEqual: compares the actual to the expected using the ! of jasmine.Matchers.toEqual * @param expected * @deprecated as of 1.0. Use not.toNotEqual() instead. */ jasmine.Matchers.prototype.toNotEqual = function(expected) { return !this.env.equals_(this.actual, expected); }; /** * Matcher that compares the actual to the expected using a regular expression. Constructs a RegExp, so takes * a pattern or a String. * * @param expected */ jasmine.Matchers.prototype.toMatch = function(expected) { return new RegExp(expected).test(this.actual); }; /** * Matcher that compares the actual to the expected using the boolean inverse of jasmine.Matchers.toMatch * @param expected * @deprecated as of 1.0. Use not.toMatch() instead. */ jasmine.Matchers.prototype.toNotMatch = function(expected) { return !(new RegExp(expected).test(this.actual)); }; /** * Matcher that compares the actual to jasmine.undefined. */ jasmine.Matchers.prototype.toBeDefined = function() { return (this.actual !== jasmine.undefined); }; /** * Matcher that compares the actual to jasmine.undefined. */ jasmine.Matchers.prototype.toBeUndefined = function() { return (this.actual === jasmine.undefined); }; /** * Matcher that compares the actual to null. */ jasmine.Matchers.prototype.toBeNull = function() { return (this.actual === null); }; /** * Matcher that boolean not-nots the actual. */ jasmine.Matchers.prototype.toBeTruthy = function() { return !!this.actual; }; /** * Matcher that boolean nots the actual. */ jasmine.Matchers.prototype.toBeFalsy = function() { return !this.actual; }; /** * Matcher that checks to see if the actual, a Jasmine spy, was called. */ jasmine.Matchers.prototype.toHaveBeenCalled = function() { if (arguments.length > 0) { throw new Error('toHaveBeenCalled does not take arguments, use toHaveBeenCalledWith'); } if (!jasmine.isSpy(this.actual)) { throw new Error('Expected a spy, but got ' + jasmine.pp(this.actual) + '.'); } this.message = function() { return [ "Expected spy " + this.actual.identity + " to have been called.", "Expected spy " + this.actual.identity + " not to have been called." ]; }; return this.actual.wasCalled; }; /** @deprecated Use expect(xxx).toHaveBeenCalled() instead */ jasmine.Matchers.prototype.wasCalled = jasmine.Matchers.prototype.toHaveBeenCalled; /** * Matcher that checks to see if the actual, a Jasmine spy, was not called. * * @deprecated Use expect(xxx).not.toHaveBeenCalled() instead */ jasmine.Matchers.prototype.wasNotCalled = function() { if (arguments.length > 0) { throw new Error('wasNotCalled does not take arguments'); } if (!jasmine.isSpy(this.actual)) { throw new Error('Expected a spy, but got ' + jasmine.pp(this.actual) + '.'); } this.message = function() { return [ "Expected spy " + this.actual.identity + " to not have been called.", "Expected spy " + this.actual.identity + " to have been called." ]; }; return !this.actual.wasCalled; }; /** * Matcher that checks to see if the actual, a Jasmine spy, was called with a set of parameters. * * @example * */ jasmine.Matchers.prototype.toHaveBeenCalledWith = function() { var expectedArgs = jasmine.util.argsToArray(arguments); if (!jasmine.isSpy(this.actual)) { throw new Error('Expected a spy, but got ' + jasmine.pp(this.actual) + '.'); } this.message = function() { if (this.actual.callCount == 0) { // todo: what should the failure message for .not.toHaveBeenCalledWith() be? is this right? test better. [xw] return [ "Expected spy to have been called with " + jasmine.pp(expectedArgs) + " but it was never called.", "Expected spy not to have been called with " + jasmine.pp(expectedArgs) + " but it was." ]; } else { return [ "Expected spy to have been called with " + jasmine.pp(expectedArgs) + " but was called with " + jasmine.pp(this.actual.argsForCall), "Expected spy not to have been called with " + jasmine.pp(expectedArgs) + " but was called with " + jasmine.pp(this.actual.argsForCall) ]; } }; return this.env.contains_(this.actual.argsForCall, expectedArgs); }; /** @deprecated Use expect(xxx).toHaveBeenCalledWith() instead */ jasmine.Matchers.prototype.wasCalledWith = jasmine.Matchers.prototype.toHaveBeenCalledWith; /** @deprecated Use expect(xxx).not.toHaveBeenCalledWith() instead */ jasmine.Matchers.prototype.wasNotCalledWith = function() { var expectedArgs = jasmine.util.argsToArray(arguments); if (!jasmine.isSpy(this.actual)) { throw new Error('Expected a spy, but got ' + jasmine.pp(this.actual) + '.'); } this.message = function() { return [ "Expected spy not to have been called with " + jasmine.pp(expectedArgs) + " but it was", "Expected spy to have been called with " + jasmine.pp(expectedArgs) + " but it was" ] }; return !this.env.contains_(this.actual.argsForCall, expectedArgs); }; /** * Matcher that checks that the expected item is an element in the actual Array. * * @param {Object} expected */ jasmine.Matchers.prototype.toContain = function(expected) { return this.env.contains_(this.actual, expected); }; /** * Matcher that checks that the expected item is NOT an element in the actual Array. * * @param {Object} expected * @deprecated as of 1.0. Use not.toNotContain() instead. */ jasmine.Matchers.prototype.toNotContain = function(expected) { return !this.env.contains_(this.actual, expected); }; jasmine.Matchers.prototype.toBeLessThan = function(expected) { return this.actual < expected; }; jasmine.Matchers.prototype.toBeGreaterThan = function(expected) { return this.actual > expected; }; /** * Matcher that checks that the expected exception was thrown by the actual. * * @param {String} expected */ jasmine.Matchers.prototype.toThrow = function(expected) { var result = false; var exception; if (typeof this.actual != 'function') { throw new Error('Actual is not a function'); } try { this.actual(); } catch (e) { exception = e; } if (exception) { result = (expected === jasmine.undefined || this.env.equals_(exception.message || exception, expected.message || expected)); } var not = this.isNot ? "not " : ""; this.message = function() { if (exception && (expected === jasmine.undefined || !this.env.equals_(exception.message || exception, expected.message || expected))) { return ["Expected function " + not + "to throw", expected ? expected.message || expected : " an exception", ", but it threw", exception.message || exception].join(' '); } else { return "Expected function to throw an exception."; } }; return result; }; jasmine.Matchers.Any = function(expectedClass) { this.expectedClass = expectedClass; }; jasmine.Matchers.Any.prototype.matches = function(other) { if (this.expectedClass == String) { return typeof other == 'string' || other instanceof String; } if (this.expectedClass == Number) { return typeof other == 'number' || other instanceof Number; } if (this.expectedClass == Function) { return typeof other == 'function' || other instanceof Function; } if (this.expectedClass == Object) { return typeof other == 'object'; } return other instanceof this.expectedClass; }; jasmine.Matchers.Any.prototype.toString = function() { return ''; }; /** * @constructor */ jasmine.MultiReporter = function() { this.subReporters_ = []; }; jasmine.util.inherit(jasmine.MultiReporter, jasmine.Reporter); jasmine.MultiReporter.prototype.addReporter = function(reporter) { this.subReporters_.push(reporter); }; (function() { var functionNames = [ "reportRunnerStarting", "reportRunnerResults", "reportSuiteResults", "reportSpecStarting", "reportSpecResults", "log" ]; for (var i = 0; i < functionNames.length; i++) { var functionName = functionNames[i]; jasmine.MultiReporter.prototype[functionName] = (function(functionName) { return function() { for (var j = 0; j < this.subReporters_.length; j++) { var subReporter = this.subReporters_[j]; if (subReporter[functionName]) { subReporter[functionName].apply(subReporter, arguments); } } }; })(functionName); } })(); /** * Holds results for a set of Jasmine spec. Allows for the results array to hold another jasmine.NestedResults * * @constructor */ jasmine.NestedResults = function() { /** * The total count of results */ this.totalCount = 0; /** * Number of passed results */ this.passedCount = 0; /** * Number of failed results */ this.failedCount = 0; /** * Was this suite/spec skipped? */ this.skipped = false; /** * @ignore */ this.items_ = []; }; /** * Roll up the result counts. * * @param result */ jasmine.NestedResults.prototype.rollupCounts = function(result) { this.totalCount += result.totalCount; this.passedCount += result.passedCount; this.failedCount += result.failedCount; }; /** * Adds a log message. * @param values Array of message parts which will be concatenated later. */ jasmine.NestedResults.prototype.log = function(values) { this.items_.push(new jasmine.MessageResult(values)); }; /** * Getter for the results: message & results. */ jasmine.NestedResults.prototype.getItems = function() { return this.items_; }; /** * Adds a result, tracking counts (total, passed, & failed) * @param {jasmine.ExpectationResult|jasmine.NestedResults} result */ jasmine.NestedResults.prototype.addResult = function(result) { if (result.type != 'log') { if (result.items_) { this.rollupCounts(result); } else { this.totalCount++; if (result.passed()) { this.passedCount++; } else { this.failedCount++; } } } this.items_.push(result); }; /** * @returns {Boolean} True if everything below passed */ jasmine.NestedResults.prototype.passed = function() { return this.passedCount === this.totalCount; }; /** * Base class for pretty printing for expectation results. */ jasmine.PrettyPrinter = function() { this.ppNestLevel_ = 0; }; /** * Formats a value in a nice, human-readable string. * * @param value */ jasmine.PrettyPrinter.prototype.format = function(value) { if (this.ppNestLevel_ > 40) { throw new Error('jasmine.PrettyPrinter: format() nested too deeply!'); } this.ppNestLevel_++; try { if (value === jasmine.undefined) { this.emitScalar('undefined'); } else if (value === null) { this.emitScalar('null'); } else if (value === jasmine.getGlobal()) { this.emitScalar(''); } else if (value instanceof jasmine.Matchers.Any) { this.emitScalar(value.toString()); } else if (typeof value === 'string') { this.emitString(value); } else if (jasmine.isSpy(value)) { this.emitScalar("spy on " + value.identity); } else if (value instanceof RegExp) { this.emitScalar(value.toString()); } else if (typeof value === 'function') { this.emitScalar('Function'); } else if (typeof value.nodeType === 'number') { this.emitScalar('HTMLNode'); } else if (value instanceof Date) { this.emitScalar('Date(' + value + ')'); } else if (value.__Jasmine_been_here_before__) { this.emitScalar(''); } else if (jasmine.isArray_(value) || typeof value == 'object') { value.__Jasmine_been_here_before__ = true; if (jasmine.isArray_(value)) { this.emitArray(value); } else { this.emitObject(value); } delete value.__Jasmine_been_here_before__; } else { this.emitScalar(value.toString()); } } finally { this.ppNestLevel_--; } }; jasmine.PrettyPrinter.prototype.iterateObject = function(obj, fn) { for (var property in obj) { if (property == '__Jasmine_been_here_before__') continue; fn(property, obj.__lookupGetter__ ? (obj.__lookupGetter__(property) != null) : false); } }; jasmine.PrettyPrinter.prototype.emitArray = jasmine.unimplementedMethod_; jasmine.PrettyPrinter.prototype.emitObject = jasmine.unimplementedMethod_; jasmine.PrettyPrinter.prototype.emitScalar = jasmine.unimplementedMethod_; jasmine.PrettyPrinter.prototype.emitString = jasmine.unimplementedMethod_; jasmine.StringPrettyPrinter = function() { jasmine.PrettyPrinter.call(this); this.string = ''; }; jasmine.util.inherit(jasmine.StringPrettyPrinter, jasmine.PrettyPrinter); jasmine.StringPrettyPrinter.prototype.emitScalar = function(value) { this.append(value); }; jasmine.StringPrettyPrinter.prototype.emitString = function(value) { this.append("'" + value + "'"); }; jasmine.StringPrettyPrinter.prototype.emitArray = function(array) { this.append('[ '); for (var i = 0; i < array.length; i++) { if (i > 0) { this.append(', '); } this.format(array[i]); } this.append(' ]'); }; jasmine.StringPrettyPrinter.prototype.emitObject = function(obj) { var self = this; this.append('{ '); var first = true; this.iterateObject(obj, function(property, isGetter) { if (first) { first = false; } else { self.append(', '); } self.append(property); self.append(' : '); if (isGetter) { self.append(''); } else { self.format(obj[property]); } }); this.append(' }'); }; jasmine.StringPrettyPrinter.prototype.append = function(value) { this.string += value; }; jasmine.Queue = function(env) { this.env = env; this.blocks = []; this.running = false; this.index = 0; this.offset = 0; this.abort = false; }; jasmine.Queue.prototype.addBefore = function(block) { this.blocks.unshift(block); }; jasmine.Queue.prototype.add = function(block) { this.blocks.push(block); }; jasmine.Queue.prototype.insertNext = function(block) { this.blocks.splice((this.index + this.offset + 1), 0, block); this.offset++; }; jasmine.Queue.prototype.start = function(onComplete) { this.running = true; this.onComplete = onComplete; this.next_(); }; jasmine.Queue.prototype.isRunning = function() { return this.running; }; jasmine.Queue.LOOP_DONT_RECURSE = true; jasmine.Queue.prototype.next_ = function() { var self = this; var goAgain = true; while (goAgain) { goAgain = false; if (self.index < self.blocks.length && !this.abort) { var calledSynchronously = true; var completedSynchronously = false; var onComplete = function () { if (jasmine.Queue.LOOP_DONT_RECURSE && calledSynchronously) { completedSynchronously = true; return; } if (self.blocks[self.index].abort) { self.abort = true; } self.offset = 0; self.index++; var now = new Date().getTime(); if (self.env.updateInterval && now - self.env.lastUpdate > self.env.updateInterval) { self.env.lastUpdate = now; self.env.setTimeout(function() { self.next_(); }, 0); } else { if (jasmine.Queue.LOOP_DONT_RECURSE && completedSynchronously) { goAgain = true; } else { self.next_(); } } }; self.blocks[self.index].execute(onComplete); calledSynchronously = false; if (completedSynchronously) { onComplete(); } } else { self.running = false; if (self.onComplete) { self.onComplete(); } } } }; jasmine.Queue.prototype.results = function() { var results = new jasmine.NestedResults(); for (var i = 0; i < this.blocks.length; i++) { if (this.blocks[i].results) { results.addResult(this.blocks[i].results()); } } return results; }; /** * Runner * * @constructor * @param {jasmine.Env} env */ jasmine.Runner = function(env) { var self = this; self.env = env; self.queue = new jasmine.Queue(env); self.before_ = []; self.after_ = []; self.suites_ = []; }; jasmine.Runner.prototype.execute = function() { var self = this; if (self.env.reporter.reportRunnerStarting) { self.env.reporter.reportRunnerStarting(this); } self.queue.start(function () { self.finishCallback(); }); }; jasmine.Runner.prototype.beforeEach = function(beforeEachFunction) { beforeEachFunction.typeName = 'beforeEach'; this.before_.splice(0,0,beforeEachFunction); }; jasmine.Runner.prototype.afterEach = function(afterEachFunction) { afterEachFunction.typeName = 'afterEach'; this.after_.splice(0,0,afterEachFunction); }; jasmine.Runner.prototype.finishCallback = function() { this.env.reporter.reportRunnerResults(this); }; jasmine.Runner.prototype.addSuite = function(suite) { this.suites_.push(suite); }; jasmine.Runner.prototype.add = function(block) { if (block instanceof jasmine.Suite) { this.addSuite(block); } this.queue.add(block); }; jasmine.Runner.prototype.specs = function () { var suites = this.suites(); var specs = []; for (var i = 0; i < suites.length; i++) { specs = specs.concat(suites[i].specs()); } return specs; }; jasmine.Runner.prototype.suites = function() { return this.suites_; }; jasmine.Runner.prototype.topLevelSuites = function() { var topLevelSuites = []; for (var i = 0; i < this.suites_.length; i++) { if (!this.suites_[i].parentSuite) { topLevelSuites.push(this.suites_[i]); } } return topLevelSuites; }; jasmine.Runner.prototype.results = function() { return this.queue.results(); }; /** * Internal representation of a Jasmine specification, or test. * * @constructor * @param {jasmine.Env} env * @param {jasmine.Suite} suite * @param {String} description */ jasmine.Spec = function(env, suite, description) { if (!env) { throw new Error('jasmine.Env() required'); } if (!suite) { throw new Error('jasmine.Suite() required'); } var spec = this; spec.id = env.nextSpecId ? env.nextSpecId() : null; spec.env = env; spec.suite = suite; spec.description = description; spec.queue = new jasmine.Queue(env); spec.afterCallbacks = []; spec.spies_ = []; spec.results_ = new jasmine.NestedResults(); spec.results_.description = description; spec.matchersClass = null; }; jasmine.Spec.prototype.getFullName = function() { return this.suite.getFullName() + ' ' + this.description + '.'; }; jasmine.Spec.prototype.results = function() { return this.results_; }; /** * All parameters are pretty-printed and concatenated together, then written to the spec's output. * * Be careful not to leave calls to jasmine.log in production code. */ jasmine.Spec.prototype.log = function() { return this.results_.log(arguments); }; jasmine.Spec.prototype.runs = function (func) { var block = new jasmine.Block(this.env, func, this); this.addToQueue(block); return this; }; jasmine.Spec.prototype.addToQueue = function (block) { if (this.queue.isRunning()) { this.queue.insertNext(block); } else { this.queue.add(block); } }; /** * @param {jasmine.ExpectationResult} result */ jasmine.Spec.prototype.addMatcherResult = function(result) { this.results_.addResult(result); }; jasmine.Spec.prototype.expect = function(actual) { var positive = new (this.getMatchersClass_())(this.env, actual, this); positive.not = new (this.getMatchersClass_())(this.env, actual, this, true); return positive; }; /** * Waits a fixed time period before moving to the next block. * * @deprecated Use waitsFor() instead * @param {Number} timeout milliseconds to wait */ jasmine.Spec.prototype.waits = function(timeout) { var waitsFunc = new jasmine.WaitsBlock(this.env, timeout, this); this.addToQueue(waitsFunc); return this; }; /** * Waits for the latchFunction to return true before proceeding to the next block. * * @param {Function} latchFunction * @param {String} optional_timeoutMessage * @param {Number} optional_timeout */ jasmine.Spec.prototype.waitsFor = function(latchFunction, optional_timeoutMessage, optional_timeout) { var latchFunction_ = null; var optional_timeoutMessage_ = null; var optional_timeout_ = null; for (var i = 0; i < arguments.length; i++) { var arg = arguments[i]; switch (typeof arg) { case 'function': latchFunction_ = arg; break; case 'string': optional_timeoutMessage_ = arg; break; case 'number': optional_timeout_ = arg; break; } } var waitsForFunc = new jasmine.WaitsForBlock(this.env, optional_timeout_, latchFunction_, optional_timeoutMessage_, this); this.addToQueue(waitsForFunc); return this; }; jasmine.Spec.prototype.fail = function (e) { var expectationResult = new jasmine.ExpectationResult({ passed: false, message: e ? jasmine.util.formatException(e) : 'Exception' }); this.results_.addResult(expectationResult); }; jasmine.Spec.prototype.getMatchersClass_ = function() { return this.matchersClass || this.env.matchersClass; }; jasmine.Spec.prototype.addMatchers = function(matchersPrototype) { var parent = this.getMatchersClass_(); var newMatchersClass = function() { parent.apply(this, arguments); }; jasmine.util.inherit(newMatchersClass, parent); jasmine.Matchers.wrapInto_(matchersPrototype, newMatchersClass); this.matchersClass = newMatchersClass; }; jasmine.Spec.prototype.finishCallback = function() { this.env.reporter.reportSpecResults(this); }; jasmine.Spec.prototype.finish = function(onComplete) { this.removeAllSpies(); this.finishCallback(); if (onComplete) { onComplete(); } }; jasmine.Spec.prototype.after = function(doAfter) { if (this.queue.isRunning()) { this.queue.add(new jasmine.Block(this.env, doAfter, this)); } else { this.afterCallbacks.unshift(doAfter); } }; jasmine.Spec.prototype.execute = function(onComplete) { var spec = this; if (!spec.env.specFilter(spec)) { spec.results_.skipped = true; spec.finish(onComplete); return; } this.env.reporter.reportSpecStarting(this); spec.env.currentSpec = spec; spec.addBeforesAndAftersToQueue(); spec.queue.start(function () { spec.finish(onComplete); }); }; jasmine.Spec.prototype.addBeforesAndAftersToQueue = function() { var runner = this.env.currentRunner(); var i; for (var suite = this.suite; suite; suite = suite.parentSuite) { for (i = 0; i < suite.before_.length; i++) { this.queue.addBefore(new jasmine.Block(this.env, suite.before_[i], this)); } } for (i = 0; i < runner.before_.length; i++) { this.queue.addBefore(new jasmine.Block(this.env, runner.before_[i], this)); } for (i = 0; i < this.afterCallbacks.length; i++) { this.queue.add(new jasmine.Block(this.env, this.afterCallbacks[i], this)); } for (suite = this.suite; suite; suite = suite.parentSuite) { for (i = 0; i < suite.after_.length; i++) { this.queue.add(new jasmine.Block(this.env, suite.after_[i], this)); } } for (i = 0; i < runner.after_.length; i++) { this.queue.add(new jasmine.Block(this.env, runner.after_[i], this)); } }; jasmine.Spec.prototype.explodes = function() { throw 'explodes function should not have been called'; }; jasmine.Spec.prototype.spyOn = function(obj, methodName, ignoreMethodDoesntExist) { if (obj == jasmine.undefined) { throw "spyOn could not find an object to spy upon for " + methodName + "()"; } if (!ignoreMethodDoesntExist && obj[methodName] === jasmine.undefined) { throw methodName + '() method does not exist'; } if (!ignoreMethodDoesntExist && obj[methodName] && obj[methodName].isSpy) { throw new Error(methodName + ' has already been spied upon'); } var spyObj = jasmine.createSpy(methodName); this.spies_.push(spyObj); spyObj.baseObj = obj; spyObj.methodName = methodName; spyObj.originalValue = obj[methodName]; obj[methodName] = spyObj; return spyObj; }; jasmine.Spec.prototype.removeAllSpies = function() { for (var i = 0; i < this.spies_.length; i++) { var spy = this.spies_[i]; spy.baseObj[spy.methodName] = spy.originalValue; } this.spies_ = []; }; /** * Internal representation of a Jasmine suite. * * @constructor * @param {jasmine.Env} env * @param {String} description * @param {Function} specDefinitions * @param {jasmine.Suite} parentSuite */ jasmine.Suite = function(env, description, specDefinitions, parentSuite) { var self = this; self.id = env.nextSuiteId ? env.nextSuiteId() : null; self.description = description; self.queue = new jasmine.Queue(env); self.parentSuite = parentSuite; self.env = env; self.before_ = []; self.after_ = []; self.children_ = []; self.suites_ = []; self.specs_ = []; }; jasmine.Suite.prototype.getFullName = function() { var fullName = this.description; for (var parentSuite = this.parentSuite; parentSuite; parentSuite = parentSuite.parentSuite) { fullName = parentSuite.description + ' ' + fullName; } return fullName; }; jasmine.Suite.prototype.finish = function(onComplete) { this.env.reporter.reportSuiteResults(this); this.finished = true; if (typeof(onComplete) == 'function') { onComplete(); } }; jasmine.Suite.prototype.beforeEach = function(beforeEachFunction) { beforeEachFunction.typeName = 'beforeEach'; this.before_.unshift(beforeEachFunction); }; jasmine.Suite.prototype.afterEach = function(afterEachFunction) { afterEachFunction.typeName = 'afterEach'; this.after_.unshift(afterEachFunction); }; jasmine.Suite.prototype.results = function() { return this.queue.results(); }; jasmine.Suite.prototype.add = function(suiteOrSpec) { this.children_.push(suiteOrSpec); if (suiteOrSpec instanceof jasmine.Suite) { this.suites_.push(suiteOrSpec); this.env.currentRunner().addSuite(suiteOrSpec); } else { this.specs_.push(suiteOrSpec); } this.queue.add(suiteOrSpec); }; jasmine.Suite.prototype.specs = function() { return this.specs_; }; jasmine.Suite.prototype.suites = function() { return this.suites_; }; jasmine.Suite.prototype.children = function() { return this.children_; }; jasmine.Suite.prototype.execute = function(onComplete) { var self = this; this.queue.start(function () { self.finish(onComplete); }); }; jasmine.WaitsBlock = function(env, timeout, spec) { this.timeout = timeout; jasmine.Block.call(this, env, null, spec); }; jasmine.util.inherit(jasmine.WaitsBlock, jasmine.Block); jasmine.WaitsBlock.prototype.execute = function (onComplete) { this.env.reporter.log('>> Jasmine waiting for ' + this.timeout + ' ms...'); this.env.setTimeout(function () { onComplete(); }, this.timeout); }; /** * A block which waits for some condition to become true, with timeout. * * @constructor * @extends jasmine.Block * @param {jasmine.Env} env The Jasmine environment. * @param {Number} timeout The maximum time in milliseconds to wait for the condition to become true. * @param {Function} latchFunction A function which returns true when the desired condition has been met. * @param {String} message The message to display if the desired condition hasn't been met within the given time period. * @param {jasmine.Spec} spec The Jasmine spec. */ jasmine.WaitsForBlock = function(env, timeout, latchFunction, message, spec) { this.timeout = timeout || env.defaultTimeoutInterval; this.latchFunction = latchFunction; this.message = message; this.totalTimeSpentWaitingForLatch = 0; jasmine.Block.call(this, env, null, spec); }; jasmine.util.inherit(jasmine.WaitsForBlock, jasmine.Block); jasmine.WaitsForBlock.TIMEOUT_INCREMENT = 10; jasmine.WaitsForBlock.prototype.execute = function(onComplete) { this.env.reporter.log('>> Jasmine waiting for ' + (this.message || 'something to happen')); var latchFunctionResult; try { latchFunctionResult = this.latchFunction.apply(this.spec); } catch (e) { this.spec.fail(e); onComplete(); return; } if (latchFunctionResult) { onComplete(); } else if (this.totalTimeSpentWaitingForLatch >= this.timeout) { var message = 'timed out after ' + this.timeout + ' msec waiting for ' + (this.message || 'something to happen'); this.spec.fail({ name: 'timeout', message: message }); this.abort = true; onComplete(); } else { this.totalTimeSpentWaitingForLatch += jasmine.WaitsForBlock.TIMEOUT_INCREMENT; var self = this; this.env.setTimeout(function() { self.execute(onComplete); }, jasmine.WaitsForBlock.TIMEOUT_INCREMENT); } }; // Mock setTimeout, clearTimeout // Contributed by Pivotal Computer Systems, www.pivotalsf.com jasmine.FakeTimer = function() { this.reset(); var self = this; self.setTimeout = function(funcToCall, millis) { self.timeoutsMade++; self.scheduleFunction(self.timeoutsMade, funcToCall, millis, false); return self.timeoutsMade; }; self.setInterval = function(funcToCall, millis) { self.timeoutsMade++; self.scheduleFunction(self.timeoutsMade, funcToCall, millis, true); return self.timeoutsMade; }; self.clearTimeout = function(timeoutKey) { self.scheduledFunctions[timeoutKey] = jasmine.undefined; }; self.clearInterval = function(timeoutKey) { self.scheduledFunctions[timeoutKey] = jasmine.undefined; }; }; jasmine.FakeTimer.prototype.reset = function() { this.timeoutsMade = 0; this.scheduledFunctions = {}; this.nowMillis = 0; }; jasmine.FakeTimer.prototype.tick = function(millis) { var oldMillis = this.nowMillis; var newMillis = oldMillis + millis; this.runFunctionsWithinRange(oldMillis, newMillis); this.nowMillis = newMillis; }; jasmine.FakeTimer.prototype.runFunctionsWithinRange = function(oldMillis, nowMillis) { var scheduledFunc; var funcsToRun = []; for (var timeoutKey in this.scheduledFunctions) { scheduledFunc = this.scheduledFunctions[timeoutKey]; if (scheduledFunc != jasmine.undefined && scheduledFunc.runAtMillis >= oldMillis && scheduledFunc.runAtMillis <= nowMillis) { funcsToRun.push(scheduledFunc); this.scheduledFunctions[timeoutKey] = jasmine.undefined; } } if (funcsToRun.length > 0) { funcsToRun.sort(function(a, b) { return a.runAtMillis - b.runAtMillis; }); for (var i = 0; i < funcsToRun.length; ++i) { try { var funcToRun = funcsToRun[i]; this.nowMillis = funcToRun.runAtMillis; funcToRun.funcToCall(); if (funcToRun.recurring) { this.scheduleFunction(funcToRun.timeoutKey, funcToRun.funcToCall, funcToRun.millis, true); } } catch(e) { } } this.runFunctionsWithinRange(oldMillis, nowMillis); } }; jasmine.FakeTimer.prototype.scheduleFunction = function(timeoutKey, funcToCall, millis, recurring) { this.scheduledFunctions[timeoutKey] = { runAtMillis: this.nowMillis + millis, funcToCall: funcToCall, recurring: recurring, timeoutKey: timeoutKey, millis: millis }; }; /** * @namespace */ jasmine.Clock = { defaultFakeTimer: new jasmine.FakeTimer(), reset: function() { jasmine.Clock.assertInstalled(); jasmine.Clock.defaultFakeTimer.reset(); }, tick: function(millis) { jasmine.Clock.assertInstalled(); jasmine.Clock.defaultFakeTimer.tick(millis); }, runFunctionsWithinRange: function(oldMillis, nowMillis) { jasmine.Clock.defaultFakeTimer.runFunctionsWithinRange(oldMillis, nowMillis); }, scheduleFunction: function(timeoutKey, funcToCall, millis, recurring) { jasmine.Clock.defaultFakeTimer.scheduleFunction(timeoutKey, funcToCall, millis, recurring); }, useMock: function() { if (!jasmine.Clock.isInstalled()) { var spec = jasmine.getEnv().currentSpec; spec.after(jasmine.Clock.uninstallMock); jasmine.Clock.installMock(); } }, installMock: function() { jasmine.Clock.installed = jasmine.Clock.defaultFakeTimer; }, uninstallMock: function() { jasmine.Clock.assertInstalled(); jasmine.Clock.installed = jasmine.Clock.real; }, real: { setTimeout: jasmine.getGlobal().setTimeout, clearTimeout: jasmine.getGlobal().clearTimeout, setInterval: jasmine.getGlobal().setInterval, clearInterval: jasmine.getGlobal().clearInterval }, assertInstalled: function() { if (!jasmine.Clock.isInstalled()) { throw new Error("Mock clock is not installed, use jasmine.Clock.useMock()"); } }, isInstalled: function() { return jasmine.Clock.installed == jasmine.Clock.defaultFakeTimer; }, installed: null }; jasmine.Clock.installed = jasmine.Clock.real; //else for IE support jasmine.getGlobal().setTimeout = function(funcToCall, millis) { if (jasmine.Clock.installed.setTimeout.apply) { return jasmine.Clock.installed.setTimeout.apply(this, arguments); } else { return jasmine.Clock.installed.setTimeout(funcToCall, millis); } }; jasmine.getGlobal().setInterval = function(funcToCall, millis) { if (jasmine.Clock.installed.setInterval.apply) { return jasmine.Clock.installed.setInterval.apply(this, arguments); } else { return jasmine.Clock.installed.setInterval(funcToCall, millis); } }; jasmine.getGlobal().clearTimeout = function(timeoutKey) { if (jasmine.Clock.installed.clearTimeout.apply) { return jasmine.Clock.installed.clearTimeout.apply(this, arguments); } else { return jasmine.Clock.installed.clearTimeout(timeoutKey); } }; jasmine.getGlobal().clearInterval = function(timeoutKey) { if (jasmine.Clock.installed.clearTimeout.apply) { return jasmine.Clock.installed.clearInterval.apply(this, arguments); } else { return jasmine.Clock.installed.clearInterval(timeoutKey); } }; jasmine.version_= { "major": 1, "minor": 0, "build": 1, "revision": 1286311016 }; ================================================ FILE: src/test/spec/bit155/attr.spec.js ================================================ /* * attr.spec.js * * Author: dave@bit155.com * * --------------------------------------------------------------------------- * * Copyright (c) 2010, David Heaton * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * * Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * * Neither the name of bit155 nor the names of its contributors * may be used to endorse or promote products derived from this software * without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ // attr describe('attr', function() { it('should copy arrays', function() { var MyClass = function() { this.data = bit155.attr(); }; var my = new MyClass(); var data = ['hello', 'jello']; my.data(data); expect(my.data()).toEqual(['hello', 'jello']); data.push('bellow'); expect(my.data()).toEqual(['hello', 'jello']); }); it('should set simple value', function() { var MyClass = function() { this.name = bit155.attr(); }; var my = new MyClass(); expect(my.name('dave')).toEqual(my); expect(my.name()).toEqual('dave'); }); it('should set array value', function() { var MyClass = function() { this.name = bit155.attr(); }; var my = new MyClass(); expect(my.name('dave', 'heaton')).toEqual(my); expect(my.name()).toEqual(['dave', 'heaton']); }); it('should use initial value', function() { var MyClass = function() { this.name = bit155.attr({ initial: 'anne' }); }; var my = new MyClass(); expect(my.name()).toEqual('anne'); }); it('should not filter initial value', function() { var MyClass = function() { this.name = bit155.attr({ initial: 'anne', filter: function(v) { return v.toUpperCase(); } }); }; var my = new MyClass(); expect(my.name()).toEqual('anne'); }); it('filter should assign different value', function() { var MyClass = function() { this.name = bit155.attr({ filter: function(v) { return v.toUpperCase(); } }); }; var my = new MyClass(); expect(my.name('dave')).toEqual(my); expect(my.name()).toEqual('DAVE'); }); it('filter validator should throw error', function() { var MyClass = function() { this.name = bit155.attr({ filter: function(v) { if (typeof v !== 'string') throw 'Bad value'; } }); }; var my = new MyClass(); expect(my.name('dave')).toEqual(my); expect(my.name()).toEqual('dave'); expect(function(){ my.name(42); }).toThrow('Bad value'); expect(my.name()).toEqual('dave'); }); it('should invoke after callback', function() { var callbackValue; var MyClass = function() { this.name = bit155.attr({ callback: function(v) { callbackValue = true; } }); }; var my = new MyClass(); expect(my.name('dave')).toEqual(my); expect(callbackValue).toEqual(true); expect(my.name()).toEqual('dave'); }); it('filter and callback should have access to object', function() { var beforeThis, afterThis; var MyClass = function() { this.name = bit155.attr({ filter: function(v) { beforeThis = this; }, callback: function(v) { afterThis = this; } }); }; var my = new MyClass(); expect(my.name('dave')).toEqual(my); expect(beforeThis).toEqual(my); expect(afterThis).toEqual(my); expect(my.name()).toEqual('dave'); }); }); ================================================ FILE: src/test/spec/bit155/csv.spec.js ================================================ /* * csv.spec.js * * Author: dave@bit155.com * * --------------------------------------------------------------------------- * * Copyright (c) 2010, David Heaton * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * * Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * * Neither the name of bit155 nor the names of its contributors * may be used to endorse or promote products derived from this software * without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ // csv describe('csv', function() { // cell describe('cell', function() { it('should encode undefined', function() { expect(bit155.csv.cell()).toEqual(''); }); it('should encode null', function() { expect(bit155.csv.cell(null)).toEqual(''); }); it('should encode number', function() { expect(bit155.csv.cell(1)).toEqual('1'); }); it('should encode string', function() { expect(bit155.csv.cell('my string')).toEqual('my string'); }); it('should keep newlines', function() { expect(bit155.csv.cell('my\nstring')).toEqual('"my\nstring"'); }); it('should not escape commas, only quote', function() { expect(bit155.csv.cell('my, string')).toEqual('"my, string"'); }); it('should escape quotes', function() { expect(bit155.csv.cell('my "string"')).toEqual('"my ""string"""'); }); it('should not escape backspace', function() { expect(bit155.csv.cell('my\\string')).toEqual('my\\string'); }); it('should escape lots', function() { expect(bit155.csv.cell('my\n"string" is, awesome\\wicked')).toEqual('"my\n""string"" is, awesome\\wicked"'); }); it('should not trim', function() { expect(bit155.csv.cell(' boo, ')).toEqual('" boo, "'); }); it('should escape multiple quotes', function() { expect(bit155.csv.cell('2.5" / 230,000 px')).toEqual('"2.5"" / 230,000 px"'); }); }); // row describe('row', function() { it('should encode empty row', function() { expect(bit155.csv.row()).toEqual(''); }); it('should encode empty row array', function() { expect(bit155.csv.row([])).toEqual(''); }); it('should encode null values varargs', function() { expect(bit155.csv.row(null, null)).toEqual(','); }); it('should encode null values array', function() { expect(bit155.csv.row([null, null])).toEqual(','); }); it('should not encode object values', function() { expect(bit155.csv.row({name: 'hello'})).toEqual('[object Object]'); }); }); // csv describe('csv', function() { it('should encode nothing', function() { expect(bit155.csv.csv()).toEqual(''); }); it('should encode an empty array', function() { expect(bit155.csv.csv([])).toEqual(''); }); it('should encode a 2d array single row', function() { expect(bit155.csv.csv([ ['one','two,too,to'] ])).toEqual('one,"two,too,to"\n'); }); it('should encode a 2d array two rows', function() { expect(bit155.csv.csv([ ['one','two'], [3, 'four or "for"'] ])).toEqual('one,two\n3,"four or ""for"""\n'); }); }); }); ================================================ FILE: src/test/spec/bit155/scraper.spec.js ================================================ /* * scraper.spec.js * * Author: dave@bit155.com * * --------------------------------------------------------------------------- * * Copyright (c) 2010, David Heaton * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * * Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * * Neither the name of bit155 nor the names of its contributors * may be used to endorse or promote products derived from this software * without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ describe('scraper', function() { describe('xpathForSelection', function() { }); }); ================================================ FILE: src/test/spec/jquery-commonAncestor.spec.js ================================================ /* * jquery-commonAncestor.spec.js * * Author: dave@bit155.com * * --------------------------------------------------------------------------- * * Copyright (c) 2010, David Heaton * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * * Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * * Neither the name of bit155 nor the names of its contributors * may be used to endorse or promote products derived from this software * without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ describe('commonAncestor', function() { it('should select common ancestor of two siblings', function() { var table1 = $(''); var table1_tr1 = $('').appendTo(table1); var table1_tr1_td1 = $('
    ').appendTo(table1_tr1); var table1_tr1_td2 = $('').appendTo(table1_tr1); var table1_tr1_td2_p = $('

    ').appendTo(table1_tr1_td2); expect($(table1_tr1_td1, table1_tr1_td1).commonAncestor().get(0)).toEqual(table1_tr1.get(0)); }); it('should select common ancestor of element and its aunt', function() { var table1 = $(''); var table1_tr1 = $('').appendTo(table1); var table1_tr1_td1 = $('
    ').appendTo(table1_tr1); var table1_tr1_td2 = $('').appendTo(table1_tr1); var table1_tr1_td2_p = $('

    ').appendTo(table1_tr1_td2); expect($(table1_tr1_td1, table1_tr1_td2_p).commonAncestor().get(0)).toEqual(table1_tr1.get(0)); }); it('should select parent as common ancestor of element and its parent', function() { var table1 = $(''); var table1_tr1 = $('').appendTo(table1); var table1_tr1_td1 = $('
    ').appendTo(table1_tr1); var table1_tr1_td2 = $('').appendTo(table1_tr1); var table1_tr1_td2_p = $('

    ').appendTo(table1_tr1_td2); expect($(table1_tr1_td1, table1_tr1).commonAncestor().get(0)).toEqual(table1_tr1.get(0)); }); it('should select no common ancestor of unrelated elements', function() { var table1_tr1_td1 = $('

    '); var table1_tr1_td2 = $(''); expect($(table1_tr1_td1, table1_tr1_td1).commonAncestor().get(0)).toBeUndefined(); }); }); ================================================ FILE: src/test/spec/jquery-serializeParams.spec.js ================================================ /* * jquery-serializeParams.spec.js * * Author: dave@bit155.com * * --------------------------------------------------------------------------- * * Copyright (c) 2010, David Heaton * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * * Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * * Neither the name of bit155 nor the names of its contributors * may be used to endorse or promote products derived from this software * without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ // serializeParams describe("serializeParams", function() { it('should ignore malformed names', function() { var form = $('
    '); var params = form.serializeParams(); expect(params).toEqual({ 'foo[': 'bar' }); }); it('should deal with typical names', function() { var form = $('
    ') .append($('')) .append($('')); var params = form.serializeParams(); expect(params).toEqual({ foo: 'bar', jabber: 'wocky' }); }); it('should create array for multiple values', function() { var form = $('') .append($('')) .append($('')); var params = form.serializeParams(); expect(params).toEqual({ foo: ['bar', 'wocky'] }); }); it('should collect collisions in a different place if there are empty brackets', function() { var form = $('') .append($('')) .append($('')) .append($('')) .append($('')); var params = form.serializeParams(); expect(params).toEqual({ 'attributes': [ { name: 'name1', type: 'type1' }, { name: 'name2', type: 'type2' } ] }); }); }); ================================================ FILE: src/test/spec/jquery-xpath.spec.js ================================================ /* * jquery-serializeParams.spec.js * * Author: dave@bit155.com * * --------------------------------------------------------------------------- * * Copyright (c) 2010, David Heaton * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * * Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * * Neither the name of bit155 nor the names of its contributors * may be used to endorse or promote products derived from this software * without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ // serializeParams describe("xpath", function() { it('should generate indices for all elements with siblings', function() { var struct = $('
    ') .append($('