Full Code of mnmldave/scraper for AI

master cb75851385c3 cached
45 files
1.1 MB
315.8k tokens
145 symbols
1 requests
Download .txt
Showing preview only (1,134K chars total). Download the full file or copy to clipboard to get everything.
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
================================================
<!DOCTYPE html>

<!-- 
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. 
-->

<html>
<head>
  <script type="text/javascript" src="chrome_ex_oauthsimple.js"></script>
  <script type="text/javascript" src="chrome_ex_oauth.js"></script>
  <script type="text/javascript" src="lib/jquery-ui-1.8.6/js/jquery-1.4.2.js"></script>
  <script type="text/javascript" src="js/shared.js"></script>
  <script type="text/javascript" src="js/bit155/attr.js"></script>
  <script type="text/javascript" src="js/bit155/scraper.js"></script>
  <script type="text/javascript" src="js/background.js"></script>
</head>
<body>
</body>
</html>

================================================
FILE: src/chrome_ex_oauth.html
================================================
<!DOCTYPE html>
<!--
 * Copyright (c) 2009 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.
-->
<html>
  <head>
    <title>OAuth Redirect Page</title>
    <style type="text/css">
      body {
        font: 16px Arial;
        color: #333;
      }
    </style>
    <script type="text/javascript" src="chrome_ex_oauthsimple.js"></script>
    <script type="text/javascript" src="chrome_ex_oauth.js"></script>
    <script type="text/javascript">
      function onLoad() {
        ChromeExOAuth.initCallbackPage();
      };
    </script>
  </head>
  <body onload="onLoad();">
    Redirecting...
  </body>
</html>


================================================
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 <explative>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:
       <code>
        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;
       </code>
     *
     * 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<pLength;j++)
                    {
                        result += pName +'="'+this._oauthEscape(this._parameters[pName][j])+'" ';
                    }
                }
                else
                {
                    result += pName + '="'+this._oauthEscape(this._parameters[pName])+'" ';
                }
            }
            return result;
        };

        // Start Private Methods.

        /** convert the parameter string into a hash of objects.
         *
         */
        this._parseParameterString = function(paramString){
            var elements = paramString.split('&');
            var result={};
            for(var element=elements.shift();element;element=elements.shift())
            {
                var keyToken=element.split('=');
                var value='';
                if (keyToken[1]) {
                    value=decodeURIComponent(keyToken[1]);
                    }
                if(result[keyToken[0]]){
                    if (!(result[keyToken[0]] instanceof Array))
                    {
                        result[keyToken[0]] = Array(result[keyToken[0]],value);
                    }
                    else
                    {
                        result[keyToken[0]].push(value);
                    }
                }
                else
                {
                    result[keyToken[0]]=value;
                }
            }
            return result;
        };

        this._oauthEscape = function(string) {
            if (string === undefined) {
                return "";
                }
            if (string instanceof Array)
            {
                throw('Array passed to _oauthEscape');
            }
            return encodeURIComponent(string).replace(/\!/g, "%21").
            replace(/\*/g, "%2A").
            replace(/'/g, "%27").
            replace(/\(/g, "%28").
            replace(/\)/g, "%29");
        };

        this._getNonce = function (length) {
            if (length === undefined) {
                length=5;
                }
            var result = "";
            var cLength = this._nonce_chars.length;
            for (var i = 0; i < length;i++) {
                var rnum = Math.floor(Math.random() *cLength);
                result += this._nonce_chars.substring(rnum,rnum+1);
            }
            this._parameters['oauth_nonce']=result;
            return result;
        };

        this._getApiKey = function() {
            if (this._secrets.consumer_key === undefined) {
                throw('No consumer_key set for OAuthSimple.');
                }
            this._parameters['oauth_consumer_key']=this._secrets.consumer_key;
            return this._parameters.oauth_consumer_key;
        };

        this._getAccessToken = function() {
            if (this._secrets['oauth_secret'] === undefined) {
                return '';
                }
            if (this._secrets['oauth_token'] === undefined) {
                throw('No oauth_token (access_token) set for OAuthSimple.');
                }
            this._parameters['oauth_token'] = this._secrets.oauth_token;
            return this._parameters.oauth_token;
        };

        this._getTimestamp = function() {
            var d = new Date();
            var ts = Math.floor(d.getTime()/1000);
            this._parameters['oauth_timestamp'] = ts;
            return ts;
        };

        this.b64_hmac_sha1 = function(k,d,_p,_z){
        // heavily optimized and compressed version of http://pajhome.org.uk/crypt/md5/sha1.js
        // _p = b64pad, _z = character size; not used here but I left them available just in case
        if(!_p){_p='=';}if(!_z){_z=8;}function _f(t,b,c,d){if(t<20){return(b&c)|((~b)&d);}if(t<40){return b^c^d;}if(t<60){return(b&c)|(b&d)|(c&d);}return b^c^d;}function _k(t){return(t<20)?1518500249:(t<40)?1859775393:(t<60)?-1894007588:-899497514;}function _s(x,y){var l=(x&0xFFFF)+(y&0xFFFF),m=(x>>16)+(y>>16)+(l>>16);return(m<<16)|(l&0xFFFF);}function _r(n,c){return(n<<c)|(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<x.length;i+=16){var o=a,p=b,q=c,r=d,s=e;for(var j=0;j<80;j++){if(j<16){w[j]=x[i+j];}else{w[j]=_r(w[j-3]^w[j-8]^w[j-14]^w[j-16],1);}var t=_s(_s(_r(a,5),_f(j,b,c,d)),_s(_s(e,w[j]),_k(j)));e=d;d=c;c=_r(b,30);b=a;a=t;}a=_s(a,o);b=_s(b,p);c=_s(c,q);d=_s(d,r);e=_s(e,s);}return[a,b,c,d,e];}function _b(s){var b=[],m=(1<<_z)-1;for(var i=0;i<s.length*_z;i+=_z){b[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<b.length*4;i+=3){var r=(((b[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<pLen; i++)
            {
                paramName=paramNames[i];
                //skip secrets.
                if (paramName.match(/\w+_secret/)) {
                    continue;
                    }
                if (this._parameters[paramName] instanceof Array)
                {
                    var sorted = this._parameters[paramName].sort();
                    var spLen = sorted.length;
                    for (var j = 0;j<spLen;j++){
                        if (ra++ > 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($('<li class="disabled">').text("No presets have been defined yet."));
  } else {
    $.each(presets, function(index, preset) {
      presetList.append($('<li class="preset">').append($('<a href="javascript:;">').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 = $('<img class="preset-handle" src="img/application-form.png">');
    var load = $('<a class="preset-load" href="javascript:;" title="Load this preset.">').text(preset.name).click(function() {
      self.options(preset.options);
      $('#presets-form-name').val(preset.name);
      $('#presets').dialog('close');
      self.scrape();
      return false;
    });
    var remove = $('<a class="preset-remove" href="javascript:;" title="Remove this preset.">').append($('<img src="img/bullet_delete.png" title="Remove preset.">')).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($('<li>').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 = $('<input>').attr('type', 'text').attr('name', 'attributes[][xpath]').attr('placeholder', 'XPath').val(xpath || '');
  var nameInput = $('<input>').attr('type', 'text').attr('name', 'attributes[][name]').attr('placeholder', 'Name (optional)').val(name || '');
  var row = $('<tr>');
  
  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($('<td nowrap>').addClass('dragHandle').text(' '));
  row.append($('<td>').append(xpathInput));
  row.append($('<td>').append(nameInput));
  row.append($('<td nowrap>')
    .append($('<a>').attr('href', 'javascript:;').click(deleteRow).html('<img src="img/bullet_delete.png">'))
    .append($('<a>').attr('href', 'javascript:;').click(addRow).html('<img src="img/bullet_add.png">'))
  );
  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) {
  $('<div class="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 = $('<thead>');
  var headerRow = $('<tr>').appendTo(thead).append('<th>&nbsp;</th>').append('<th>&nbsp;</th>');
  $.each(attributes, function() {
    headerRow.append($('<th>').text(this.name));
  });

  // body
  var tbody = $('<tbody>');
  $.each(results, function(i,result) {
    var row = $('<tr>').appendTo(tbody);
    var tools = $('<td class="tools" nowrap>').appendTo(row);

    // tools
    tools.append($('<img src="img/highlighter-small.png" title="Highlight in document.">').click(function() {
      chrome.tabs.sendRequest(self.tabId(), { command: 'scraperHighlight', payload: { xpath: result.xpath } });
    }));
    
    // index
    row.append($('<td class="index" nowrap>').text(i + 1));
    
    // attributes
    $.each(attributes, function(j,attribute) {
      var value = result.values[j];
      var cell = $('<td>').text(value);
      
      row.append(cell);
    });
  });

  var url = /^https?:\/\/[^\s]+$/i;
  var table = $('<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($('<a>').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 = $('<div>').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($('<div style="margin: 30px; text-align: center"><img src="img/progress.gif"></div>'));
    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($('<a>').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($('<a href="http://api.jquery.com/category/selectors/" target="_blank">').text('jQuery Reference'));
    } else if (lang === 'xpath') {
      $('#options-language-help').append($('<a href="http://www.stylusstudio.com/docs/v62/d_xpath15.html" target="_blank">').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<iLen ; i++ )
				{
					if ( an[i].childNodes.length !== 0 )
					{
						an[i].childNodes[0].className = 
							( oSettings._iDisplayStart === 0 ) ? 
							oSettings.oClasses.sPagePrevDisabled : oSettings.oClasses.sPagePrevEnabled;
						
						an[i].childNodes[1].className = 
							( oSettings.fnDisplayEnd() == oSettings.fnRecordsDisplay() ) ? 
							oSettings.oClasses.sPageNextDisabled : oSettings.oClasses.sPageNextEnabled;
					}
				}
			}
		},
		
		
		/*
		 * Variable: iFullNumbersShowPages
		 * Purpose:  Change the number of pages which can be seen
	 	 * Scope:    jQuery.fn.dataTableExt.oPagination
		 */
		"iFullNumbersShowPages": 5,
		
		/*
		 * Variable: full_numbers
		 * Purpose:  Full numbers pagination
	 	 * Scope:    jQuery.fn.dataTableExt.oPagination
		 */
		"full_numbers": {
			/*
			 * Function: oPagination.full_numbers.fnInit
			 * Purpose:  Initalise dom elements required for pagination with a list of the pages
			 * 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 nFirst = document.createElement( 'span' );
				var nPrevious = document.createElement( 'span' );
				var nList = document.createElement( 'span' );
				var nNext = document.createElement( 'span' );
				var nLast = document.createElement( 'span' );
				
				nFirst.innerHTML = oSettings.oLanguage.oPaginate.sFirst;
				nPrevious.innerHTML = oSettings.oLanguage.oPaginate.sPrevious;
				nNext.innerHTML = oSettings.oLanguage.oPaginate.sNext;
				nLast.innerHTML = oSettings.oLanguage.oPaginate.sLast;
				
				var oClasses = oSettings.oClasses;
				nFirst.className = oClasses.sPageButton+" "+oClasses.sPageFirst;
				nPrevious.className = oClasses.sPageButton+" "+oClasses.sPagePrevious;
				nNext.className= oClasses.sPageButton+" "+oClasses.sPageNext;
				nLast.className = oClasses.sPageButton+" "+oClasses.sPageLast;
				
				nPaging.appendChild( nFirst );
				nPaging.appendChild( nPrevious );
				nPaging.appendChild( nList );
				nPaging.appendChild( nNext );
				nPaging.appendChild( nLast );
				
				$(nFirst).click( function () {
					if ( oSettings.oApi._fnPageChange( oSettings, "first" ) )
					{
						fnCallbackDraw( oSettings );
					}
				} );
				
				$(nPrevious).click( function() {
					if ( oSettings.oApi._fnPageChange( oSettings, "previous" ) )
					{
						fnCallbackDraw( oSettings );
					}
				} );
				
				$(nNext).click( function() {
					if ( oSettings.oApi._fnPageChange( oSettings, "next" ) )
					{
						fnCallbackDraw( oSettings );
					}
				} );
				
				$(nLast).click( function() {
					if ( oSettings.oApi._fnPageChange( oSettings, "last" ) )
					{
						fnCallbackDraw( oSettings );
					}
				} );
				
				/* Take the brutal approach to cancelling text selection */
				$('span', nPaging)
					.bind( 'mousedown', function () { return false; } )
					.bind( 'selectstart', function () { return false; } );
				
				/* ID the first elements only */
				if ( oSettings.sTableId !== '' && typeof oSettings.aanFeatures.p == "undefined" )
				{
					nPaging.setAttribute( 'id', oSettings.sTableId+'_paginate' );
					nFirst.setAttribute( 'id', oSettings.sTableId+'_first' );
					nPrevious.setAttribute( 'id', oSettings.sTableId+'_previous' );
					nNext.setAttribute( 'id', oSettings.sTableId+'_next' );
					nLast.setAttribute( 'id', oSettings.sTableId+'_last' );
				}
			},
			
			/*
			 * Function: oPagination.full_numbers.fnUpdate
			 * Purpose:  Update the list of page buttons shows
			 * 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;
				}
				
				var iPageCount = _oExt.oPagination.iFullNumbersShowPages;
				var iPageCountHalf = Math.floor(iPageCount / 2);
				var iPages = Math.ceil((oSettings.fnRecordsDisplay()) / oSettings._iDisplayLength);
				var iCurrentPage = Math.ceil(oSettings._iDisplayStart / oSettings._iDisplayLength) + 1;
				var sList = "";
				var iStartButton, iEndButton, i, iLen;
				var oClasses = oSettings.oClasses;
				
				/* Pages calculation */
				if (iPages < iPageCount)
				{
					iStartButton = 1;
					iEndButton = iPages;
				}
				else
				{
					if (iCurrentPage <= iPageCountHalf)
					{
						iStartButton = 1;
						iEndButton = iPageCount;
					}
					else
					{
						if (iCurrentPage >= (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 += '<span class="'+oClasses.sPageButton+'">'+i+'</span>';
					}
					else
					{
						sList += '<span class="'+oClasses.sPageButtonActive+'">'+i+'</span>';
					}
				}
				
				/* 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<iLen ; i++ )
				{
					if ( an[i].childNodes.length === 0 )
					{
						continue;
					}
					
					/* Build up the dynamic list forst - html and listeners */
					var qjPaginateList = $('span:eq(2)', an[i]);
					qjPaginateList.html( sList );
					$('span', qjPaginateList).click( fnClick ).bind( 'mousedown', fnFalse )
						.bind( 'selectstart', fnFalse );
					
					/* Update the 'premanent botton's classes */
					anButtons = an[i].getElementsByTagName('span');
					anStatic = [
						anButtons[0], anButtons[1], 
						anButtons[anButtons.length-2], anButtons[anButtons.length-1]
					];
					$(anStatic).removeClass( oClasses.sPageButton+" "+oClasses.sPageButtonActive+" "+oClasses.sPageButtonStaticDisabled );
					if ( iCurrentPage == 1 )
					{
						anStatic[0].className += " "+oClasses.sPageButtonStaticDisabled;
						anStatic[1].className += " "+oClasses.sPageButtonStaticDisabled;
					}
					else
					{
						anStatic[0].className += " "+oClasses.sPageButton;
						anStatic[1].className += " "+oClasses.sPageButton;
					}
					
					if ( iPages === 0 || iCurrentPage == iPages || oSettings._iDisplayLength == -1 )
					{
						anStatic[2].className += " "+oClasses.sPageButtonStaticDisabled;
						anStatic[3].className += " "+oClasses.sPageButtonStaticDisabled;
					}
					else
					{
						anStatic[2].className += " "+oClasses.sPageButton;
						anStatic[3].className += " "+oClasses.sPageButton;
					}
				}
			}
		}
	};
	
	/*
	 * Variable: oSort
	 * Purpose:  Wrapper for the sorting functions that can be used in DataTables
	 * Scope:    jQuery.fn.dataTableExt
	 * Notes:    The functions provided in this object are basically standard javascript sort
	 *   functions - they expect two inputs which they then compare and then return a priority
	 *   result. For each sort method added, two functions need to be defined, an ascending sort and
	 *   a descending sort.
	 */
	_oExt.oSort = {
		/*
		 * text sorting
		 */
		"string-asc": function ( a, b )
		{
			var x = a.toLowerCase();
			var y = b.toLowerCase();
			return ((x < y) ? -1 : ((x > 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<sData.length ; i++ ) 
			{
				Char = sData.charAt(i); 
				if (sValidChars.indexOf(Char) == -1) 
				{
					return null;
				}
				
				/* Only allowed one decimal place... */
				if ( Char == "." )
				{
					if ( bDecimal )
					{
						return null;
					}
					bDecimal = true;
				}
			}
			
			return 'numeric';
		},
		
		/*
		 * Function: -
		 * Purpose:  Check to see if a string is actually a formatted date
		 * Returns:  string:'date' or null
		 * Inputs:   string:sText - string to check
		 */
		function ( sData )
		{
			var iParse = Date.parse(sData);
			if ( (iParse !== null && !isNaN(iParse)) || sData.length === 0 )
			{
				return 'date';
			}
			return null;
		},
		
		/*
		 * Function: -
		 * Purpose:  Check to see if a string should be treated as an HTML string
		 * Returns:  string:'html' or null
		 * Inputs:   string:sText - string to check
		 */
		function ( sData )
		{
			if ( sData.indexOf('<') != -1 && sData.indexOf('>') != -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<iLen ; i++ )
		{
			sThis += fnZPad( aThis[i], 3 );
			sThat += fnZPad( aThat[i], 3 );
		}
		
		return parseInt(sThis, 10) >= 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>', '<lf<t>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<iLen ; i++ )
					{
						if ( i%3 === 0 && i !== 0 )
						{
							out = ','+out;
						}
						out = a[iLen-i-1]+out;
					}
				}
				return out;
			};
			
			/*
			 * Variable: aLengthMenu
			 * Purpose:  List of options that can be used for the user selectable length menu
			 * Scope:    jQuery.dataTable.classSettings
			 * Note:     This varaible can take for form of a 1D array, in which case the value and the 
			 *   displayed value in the menu are the same, or a 2D array in which case the value comes
			 *   from the first array, and the displayed value to the end user comes from the second
			 *   array. 2D example: [ [ 10, 25, 50, 100, -1 ], [ 10, 25, 50, 100, 'All' ] ];
			 */
			this.aLengthMenu = [ 10, 25, 50, 100 ];
			
			/*
			 * Variable: iDraw
			 * Purpose:  Counter for the draws that the table does. Also used as a tracker for
			 *   server-side processing
			 * Scope:    jQuery.dataTable.classSettings
			 */
			this.iDraw = 0;
			
			/*
			 * Variable: bDrawing
			 * Purpose:  Indicate if a redraw is being done - useful for Ajax
			 * Scope:    jQuery.dataTable.classSettings
			 */
			this.bDrawing = 0;
			
			/*
			 * Variable: iDrawError
			 * Purpose:  Last draw error
			 * Scope:    jQuery.dataTable.classSettings
			 */
			this.iDrawError = -1;
			
			/*
			 * Variable: _iDisplayLength, _iDisplayStart, _iDisplayEnd
			 * Purpose:  Display length variables
			 * Scope:    jQuery.dataTable.classSettings
			 * Notes:    These variable must NOT be used externally to get the data length. Rather, use
			 *   the fnRecordsTotal() (etc) functions.
			 */
			this._iDisplayLength = 10;
			this._iDisplayStart = 0;
			this._iDisplayEnd = 10;
			
			/*
			 * Variable: _iRecordsTotal, _iRecordsDisplay
			 * Purpose:  Display length variables used for server side processing
			 * Scope:    jQuery.dataTable.classSettings
			 * Notes:    These variable must NOT be used externally to get the data length. Rather, use
			 *   the fnRecordsTotal() (etc) functions.
			 */
			this._iRecordsTotal = 0;
			this._iRecordsDisplay = 0;
			
			/*
			 * Variable: bJUI
			 * Purpose:  Should we add the markup needed for jQuery UI theming?
			 * Scope:    jQuery.dataTable.classSettings
			 */
			this.bJUI = false;
			
			/*
			 * Variable: bJUI
			 * Purpose:  Should we add the markup needed for jQuery UI theming?
			 * Scope:    jQuery.dataTable.classSettings
			 */
			this.oClasses = _oExt.oStdClasses;
			
			/*
			 * Variable: bFiltered and bSorted
			 * Purpose:  Flags to allow callback functions to see what actions have been performed
			 * Scope:    jQuery.dataTable.classSettings
			 */
			this.bFiltered = false;
			this.bSorted = false;
			
			/*
			 * Variable: oInit
			 * Purpose:  Initialisation object that is used for the table
			 * Scope:    jQuery.dataTable.classSettings
			 */
			this.oInit = null;
		}
		
		/*
		 * Variable: oApi
		 * Purpose:  Container for publicly exposed 'private' functions
		 * Scope:    jQuery.dataTable
		 */
		this.oApi = {};
		
		
		/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
		 * Section - API functions
		 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
		
		/*
		 * Function: fnDraw
		 * Purpose:  Redraw the table
		 * Returns:  -
		 * Inputs:   bool:bComplete - Refilter and resort (if enabled) the table before the draw.
		 *             Optional: default - true
		 */
		this.fnDraw = function( bComplete )
		{
			var oSettings = _fnSettingsFromNode( this[_oExt.iApiIndex] );
			if ( typeof bComplete != 'undefined' && bComplete === false )
			{
				_fnCalculateEnd( oSettings );
				_fnDraw( oSettings );
			}
			else
			{
				_fnReDraw( oSettings );
			}
		};
		
		/*
		 * Function: fnFilter
		 * Purpose:  Filter the input based on data
		 * Returns:  -
		 * Inputs:   string:sInput - string to filter the table on
		 *           int:iColumn - optional - column to limit filtering to
		 *           bool:bRegex - optional - treat as regular expression or not - default false
		 *           bool:bSmart - optional - perform smart filtering or not - default true
		 *           bool:bShowGlobal - optional - show the input global filter in it's input box(es)
		 *              - default true
		 */
		this.fnFilter = function( sInput, iColumn, bRegex, bSmart, bShowGlobal )
		{
			var oSettings = _fnSettingsFromNode( this[_oExt.iApiIndex] );
			
			if ( !oSettings.oFeatures.bFilter )
			{
				return;
			}
			
			if ( typeof bRegex == 'undefined' )
			{
				bRegex = false;
			}
			
			if ( typeof bSmart == 'undefined' )
			{
				bSmart = true;
			}
			
			if ( typeof bShowGlobal == 'undefined' )
			{
				bShowGlobal = true;
			}
			
			if ( typeof iColumn == "undefined" || iColumn === null )
			{
				/* Global filter */
				_fnFilterComplete( oSettings, {
					"sSearch":sInput,
					"bRegex": bRegex,
					"bSmart": bSmart
				}, 1 );
				
				if ( bShowGlobal && typeof oSettings.aanFeatures.f != 'undefined' )
				{
					var n = oSettings.aanFeatures.f;
					for ( var i=0, iLen=n.length ; i<iLen ; i++ )
					{
						$('input', n[i]).val( sInput );
					}
				}
			}
			else
			{
				/* Single column filter */
				oSettings.aoPreSearchCols[ iColumn ].sSearch = sInput;
				oSettings.aoPreSearchCols[ iColumn ].bRegex = bRegex;
				oSettings.aoPreSearchCols[ iColumn ].bSmart = bSmart;
				_fnFilterComplete( oSettings, oSettings.oPreviousSearch, 1 );
			}
		};
		
		/*
		 * Function: fnSettings
		 * Purpose:  Get the settings for a particular table for extern. manipulation
		 * Returns:  -
		 * Inputs:   -
		 */
		this.fnSettings = function( nNode  )
		{
			return _fnSettingsFromNode( this[_oExt.iApiIndex] );
		};
		
		/*
		 * Function: fnVersionCheck
		 * Notes:    The function is the same as the 'static' function provided in the ext variable
		 */
		this.fnVersionCheck = _oExt.fnVersionCheck;
		
		/*
		 * Function: fnSort
		 * Purpose:  Sort the table by a particular row
		 * Returns:  -
		 * Inputs:   int:iCol - the data index to sort on. Note that this will
		 *   not match the 'display index' if you have hidden data entries
		 */
		this.fnSort = function( aaSort )
		{
			var oSettings = _fnSettingsFromNode( this[_oExt.iApiIndex] );
			oSettings.aaSorting = aaSort;
			_fnSort( oSettings );
		};
		
		/*
		 * Function: fnSortListener
		 * Purpose:  Attach a sort listener to an element for a given column
		 * Returns:  -
		 * Inputs:   node:nNode - the element to attach the sort listener to
		 *           int:iColumn - the column that a click on this node will sort on
		 *           function:fnCallback - callback function when sort is run - optional
		 */
		this.fnSortListener = function( nNode, iColumn, fnCallback )
		{
			_fnSortAttachListener( _fnSettingsFromNode( this[_oExt.iApiIndex] ), nNode, iColumn,
			 	fnCallback );
		};
		
		/*
		 * Function: fnAddData
		 * Purpose:  Add new row(s) into the table
		 * Returns:  array int: array of indexes (aoData) which have been added (zero length on error)
		 * Inputs:   array:mData - the data to be added. The length must match
		 *               the original data from the DOM
		 *             or
		 *             array array:mData - 2D array of data to be added
		 *           bool:bRedraw - redraw the table or not - default true
		 * Notes:    Warning - the refilter here will cause the table to redraw
		 *             starting at zero
		 * Notes:    Thanks to Yekimov Denis for contributing the basis for this function!
		 */
		this.fnAddData = function( mData, bRedraw )
		{
			if ( mData.length === 0 )
			{
				return [];
			}
			
			var aiReturn = [];
			var iTest;
			
			/* Find settings from table node */
			var oSettings = _fnSettingsFromNode( this[_oExt.iApiIndex] );
			
			/* Check if we want to add multiple rows or not */
			if ( typeof mData[0] == "object" )
			{
				for ( var i=0 ; i<mData.length ; i++ )
				{
					iTest = _fnAddData( oSettings, mData[i] );
					if ( iTest == -1 )
					{
						return aiReturn;
					}
					aiReturn.push( iTest );
				}
			}
			else
			{
				iTest = _fnAddData( oSettings, mData );
				if ( iTest == -1 )
				{
					return aiReturn;
				}
				aiReturn.push( iTest );
			}
			
			oSettings.aiDisplay = oSettings.aiDisplayMaster.slice();
			
			if ( typeof bRedraw == 'undefined' || bRedraw )
			{
				_fnReDraw( oSettings );
			}
			return aiReturn;
		};
		
		/*
		 * Function: fnDeleteRow
		 * Purpose:  Remove a row for the table
		 * Returns:  array:aReturn - the row that was deleted
		 * Inputs:   mixed:mTarget - 
		 *             int: - index of aoData to be deleted, or
		 *             node(TR): - TR element you want to delete
		 *           function:fnCallBack - callback function - default null
		 *           bool:bRedraw - redraw the table or not - default true
		 */
		this.fnDeleteRow = function( mTarget, fnCallBack, bRedraw )
		{
			/* Find settings from table node */
			var oSettings = _fnSettingsFromNode( this[_oExt.iApiIndex] );
			var i, iAODataIndex;
			
			iAODataIndex = (typeof mTarget == 'object') ? 
				_fnNodeToDataIndex(oSettings, mTarget) : mTarget;
			
			/* Return the data array from this row */
			var oData = oSettings.aoData.splice( iAODataIndex, 1 );
			
			/* Remove the target row from the search array */
			var iDisplayIndex = $.inArray( iAODataIndex, oSettings.aiDisplay );
			oSettings.asDataSearch.splice( iDisplayIndex, 1 );
			
			/* Delete from the display arrays */
			_fnDeleteIndex( oSettings.aiDisplayMaster, iAODataIndex );
			_fnDeleteIndex( oSettings.aiDisplay, iAODataIndex );
			
			/* If there is a user callback function - call it */
			if ( typeof fnCallBack == "function" )
			{
				fnCallBack.call( this, oSettings, oData );
			}
			
			/* Check for an 'overflow' they case for dislaying the table */
			if ( oSettings._iDisplayStart >= oSettings.aiDisplay.length )
			{
				oSettings._iDisplayStart -= oSettings._iDisplayLength;
				if ( oSettings._iDisplayStart < 0 )
				{
					oSettings._iDisplayStart = 0;
				}
			}
			
			if ( typeof bRedraw == 'undefined' || bRedraw )
			{
				_fnCalculateEnd( oSettings );
				_fnDraw( oSettings );
			}
			
			return oData;
		};
		
		/*
		 * Function: fnClearTable
		 * Purpose:  Quickly and simply clear a table
		 * Returns:  -
		 * Inputs:   bool:bRedraw - redraw the table or not - default true
		 * Notes:    Thanks to Yekimov Denis for contributing the basis for this function!
		 */
		this.fnClearTable = function( bRedraw )
		{
			/* Find settings from table node */
			var oSettings = _fnSettingsFromNode( this[_oExt.iApiIndex] );
			_fnClearTable( oSettings );
			
			if ( typeof bRedraw == 'undefined' || bRedraw )
			{
				_fnDraw( oSettings );
			}
		};
		
		/*
		 * Function: fnOpen
		 * Purpose:  Open a display row (append a row after the row in question)
		 * Returns:  node:nNewRow - the row opened
		 * Inputs:   node:nTr - the table row to 'open'
		 *           string:sHtml - the HTML to put into the row
		 *           string:sClass - class to give the new TD cell
		 */
		this.fnOpen = function( nTr, sHtml, sClass )
		{
			/* Find settings from table node */
			var oSettings = _fnSettingsFromNode( this[_oExt.iApiIndex] );
			
			/* the old open one if there is one */
			this.fnClose( nTr );
			
			var nNewRow = document.createElement("tr");
			var nNewCell = document.createElement("td");
			nNewRow.appendChild( nNewCell );
			nNewCell.className = sClass;
			nNewCell.colSpan = _fnVisbleColumns( oSettings );
			nNewCell.innerHTML = sHtml;
			
			/* If the nTr isn't on the page at the moment - then we don't insert at the moment */
			var nTrs = $('tr', oSettings.nTBody);
			if ( $.inArray(nTr, nTrs) != -1 )
			{
				$(nNewRow).insertAfter(nTr);
			}
			
			oSettings.aoOpenRows.push( {
				"nTr": nNewRow,
				"nParent": nTr
			} );
			
			return nNewRow;
		};
		
		/*
		 * Function: fnClose
		 * Purpose:  Close a display row
		 * Returns:  int: 0 (success) or 1 (failed)
		 * Inputs:   node:nTr - the table row to 'close'
		 */
		this.fnClose = function( nTr )
		{
			/* Find settings from table node */
			var oSettings = _fnSettingsFromNode( this[_oExt.iApiIndex] );
			
			for ( var i=0 ; i<oSettings.aoOpenRows.length ; i++ )
			{
				if ( oSettings.aoOpenRows[i].nParent == nTr )
				{
					var nTrParent = oSettings.aoOpenRows[i].nTr.parentNode;
					if ( nTrParent )
					{
						/* Remove it if it is currently on display */
						nTrParent.removeChild( oSettings.aoOpenRows[i].nTr );
					}
					oSettings.aoOpenRows.splice( i, 1 );
					return 0;
				}
			}
			return 1;
		};
		
		/*
		 * Function: fnGetData
		 * Purpose:  Return an array with the data which is used to make up the table
		 * Returns:  array array string: 2d data array ([row][column]) or array string: 1d data array
		 *           or
		 *           array string (if iRow specified)
		 * Inputs:   mixed:mRow - optional - if not present, then the full 2D array for the table 
		 *             if given then:
		 *               int: - return 1D array for aoData entry of this index
		 *               node(TR): - return 1D array for this TR element
		 * Inputs:   int:iRow - optional - if present then the array returned will be the data for
		 *             the row with the index 'iRow'
		 */
		this.fnGetData = function( mRow )
		{
			var oSettings = _fnSettingsFromNode( this[_oExt.iApiIndex] );
			
			if ( typeof mRow != 'undefined' )
			{
				var iRow = (typeof mRow == 'object') ? 
					_fnNodeToDataIndex(oSettings, mRow) : mRow;
				return oSettings.aoData[iRow]._aData;
			}
			return _fnGetDataMaster( oSettings );
		};
		
		/*
		 * Function: fnGetNodes
		 * Purpose:  Return an array with the TR nodes used for drawing the table
		 * Returns:  array node: TR elements
		 *           or
		 *           node (if iRow specified)
		 * Inputs:   int:iRow - optional - if present then the array returned will be the node for
		 *             the row with the index 'iRow'
		 */
		this.fnGetNodes = function( iRow )
		{
			var oSettings = _fnSettingsFromNode( this[_oExt.iApiIndex] );
			
			if ( typeof iRow != 'undefined' )
			{
				return oSettings.aoData[iRow].nTr;
			}
			return _fnGetTrNodes( oSettings );
		};
		
		/*
		 * Function: fnGetPosition
		 * Purpose:  Get the array indexes of a particular cell from it's DOM element
		 * Returns:  int: - row index, or array[ int, int, int ]: - row index, column index (visible)
		 *             and column index including hidden columns
		 * Inputs:   node:nNode - this can either be a TR or a TD in the table, the return is
		 *             dependent on this input
		 */
		this.fnGetPosition = function( nNode )
		{
			var oSettings = _fnSettingsFromNode( this[_oExt.iApiIndex] );
			var i;
			
			if ( nNode.nodeName.toUpperCase() == "TR" )
			{
				return _fnNodeToDataIndex(oSettings, nNode);
			}
			else if ( nNode.nodeName.toUpperCase() == "TD" )
			{
				var iDataIndex = _fnNodeToDataIndex(oSettings, nNode.parentNode);
				var iCorrector = 0;
				for ( var j=0 ; j<oSettings.aoColumns.length ; j++ )
				{
					if ( oSettings.aoColumns[j].bVisible )
					{
						if ( oSettings.aoData[iDataIndex].nTr.getElementsByTagName('td')[j-iCorrector] == nNode )
						{
							return [ iDataIndex, j-iCorrector, j ];
						}
					}
					else
					{
						iCorrector++;
					}
				}
			}
			return null;
		};
		
		/*
		 * Function: fnUpdate
		 * Purpose:  Update a table cell or row
		 * Returns:  int: 0 okay, 1 error
		 * Inputs:   array string 'or' string:mData - data to update the cell/row with
		 *           mixed:mRow - 
		 *             int: - index of aoData to be updated, or
		 *             node(TR): - TR element you want to update
		 *           int:iColumn - the column to update - optional (not used of mData is 2D)
		 *           bool:bRedraw - redraw the table or not - default true
		 *           bool:bAction - perform predraw actions or not (you will want this as 'true' if
		 *             you have bRedraw as true) - default true
		 */
		this.fnUpdate = function( mData, mRow, iColumn, bRedraw, bAction )
		{
			var oSettings = _fnSettingsFromNode( this[_oExt.iApiIndex] );
			var iVisibleColumn;
			var sDisplay;
			var iRow = (typeof mRow == 'object') ? 
				_fnNodeToDataIndex(oSettings, mRow) : mRow;
			
			if ( typeof mData != 'object' )
			{
				sDisplay = mData;
				oSettings.aoData[iRow]._aData[iColumn] = sDisplay;
				
				if ( oSettings.aoColumns[iColumn].fnRender !== null )
				{
					sDisplay = oSettings.aoColumns[iColumn].fnRender( {
						"iDataRow": iRow,
						"iDataColumn": iColumn,
						"aData": oSettings.aoData[iRow]._aData,
						"oSettings": oSettings
					} );
					
					if ( oSettings.aoColumns[iColumn].bUseRendered )
					{
						oSettings.aoData[iRow]._aData[iColumn] = sDisplay;
					}
				}
				
				iVisibleColumn = _fnColumnIndexToVisible( oSettings, iColumn );
				if ( iVisibleColumn !== null )
				{
					oSettings.aoData[iRow].nTr.getElementsByTagName('td')[iVisibleColumn].innerHTML = 
						sDisplay;
				}
			}
			else
			{
				if ( mData.length != oSettings.aoColumns.length )
				{
					_fnLog( oSettings, 0, 'An array passed to fnUpdate must have the same number of '+
						'columns as the table in question - in this case '+oSettings.aoColumns.length );
					return 1;
				}
				
				for ( var i=0 ; i<mData.length ; i++ )
				{
					sDisplay = mData[i];
					oSettings.aoData[iRow]._aData[i] = sDisplay;
					
					if ( oSettings.aoColumns[i].fnRender !== null )
					{
						sDisplay = oSettings.aoColumns[i].fnRender( {
							"iDataRow": iRow,
							"iDataColumn": i,
							"aData": oSettings.aoData[iRow]._aData,
							"oSettings": oSettings
						} );
						
						if ( oSettings.aoColumns[i].bUseRendered )
						{
							oSettings.aoData[iRow]._aData[i] = sDisplay;
						}
					}
					
					iVisibleColumn = _fnColumnIndexToVisible( oSettings, i );
					if ( iVisibleColumn !== null )
					{
						oSettings.aoData[iRow].nTr.getElementsByTagName('td')[iVisibleColumn].innerHTML = 
							sDisplay;
					}
				}
			}
			
			/* Modify the search index for this row (strictly this is likely not needed, since fnReDraw
			 * will rebuild the search array - however, the redraw might be disabled by the user)
			 */
			var iDisplayIndex = $.inArray( iRow, oSettings.aiDisplay );
			oSettings.asDataSearch[iDisplayIndex] = _fnBuildSearchRow( oSettings, 
				oSettings.aoData[iRow]._aData );
			
			/* Perform pre-draw actions */
			if ( typeof bAction == 'undefined' || bAction )
			{
				_fnAjustColumnSizing( oSettings );
			}
			
			/* Redraw the table */
			if ( typeof bRedraw == 'undefined' || bRedraw )
			{
				_fnReDraw( oSettings );
			}
			return 0;
		};
		
		
		/*
		 * Function: fnShowColoumn
		 * Purpose:  Show a particular column
		 * Returns:  -
		 * Inputs:   int:iCol - the column whose display should be changed
		 *           bool:bShow - show (true) or hide (false) the column
		 *           bool:bRedraw - redraw the table or not - default true
		 */
		this.fnSetColumnVis = function ( iCol, bShow, bRedraw )
		{
			var oSettings = _fnSettingsFromNode( this[_oExt.iApiIndex] );
			var i, iLen;
			var iColumns = oSettings.aoColumns.length;
			var nTd, anTds;
			
			/* No point in doing anything if we are requesting what is already true */
			if ( oSettings.aoColumns[iCol].bVisible == bShow )
			{
				return;
			}
			
			var nTrHead = $('>tr', oSettings.nTHead)[0];
			var nTrFoot = $('>tr', oSettings.nTFoot)[0];
			var anTheadTh = [];
			var anTfootTh = [];
			for ( i=0 ; i<iColumns ; i++ )
			{
				anTheadTh.push( oSettings.aoColumns[i].nTh );
				anTfootTh.push( oSettings.aoColumns[i].nTf );
			}
			
			/* Show the column */
			if ( bShow )
			{
				var iInsert = 0;
				for ( i=0 ; i<iCol ; i++ )
				{
					if ( oSettings.aoColumns[i].bVisible )
					{
						iInsert++;
					}
				}
				
				/* Need to decide if we should use appendChild or insertBefore */
				if ( iInsert >= _fnVisbleColumns( oSettings ) )
				{
					nTrHead.appendChild( anTheadTh[iCol] );
					if ( nTrFoot )
					{
						nTrFoot.appendChild( anTfootTh[iCol] );
					}
					
					for ( i=0, iLen=oSettings.aoData.length ; i<iLen ; i++ )
					{
						nTd = oSettings.aoData[i]._anHidden[iCol];
						oSettings.aoData[i].nTr.appendChild( nTd );
					}
				}
				else
				{
					/* Which coloumn should we be inserting before? */
					var iBefore;
					for ( i=iCol ; i<iColumns ; i++ )
					{
						iBefore = _fnColumnIndexToVisible( oSettings, i );
						if ( iBefore !== null )
						{
							break;
						}
					}
					
					nTrHead.insertBefore( anTheadTh[iCol], nTrHead.getElementsByTagName('th')[iBefore] );
					if ( nTrFoot )
					{
						nTrFoot.insertBefore( anTfootTh[iCol], nTrFoot.getElementsByTagName('th')[iBefore] );
					}
					
					anTds = _fnGetTdNodes( oSettings );
					for ( i=0, iLen=oSettings.aoData.length ; i<iLen ; i++ )
					{
						nTd = oSettings.aoData[i]._anHidden[iCol];
						oSettings.aoData[i].nTr.insertBefore( nTd, $('>td:eq('+iBefore+')', 
							oSettings.aoData[i].nTr)[0] );
					}
				}
				
				oSettings.aoColumns[iCol].bVisible = true;
			}
			else
			{
				/* Remove a column from display */
				nTrHead.removeChild( anTheadTh[iCol] );
				if ( nTrFoot )
				{
					nTrFoot.removeChild( anTfootTh[iCol] );
				}
				
				anTds = _fnGetTdNodes( oSettings );
				for ( i=0, iLen=oSettings.aoData.length ; i<iLen ; i++ )
				{
					nTd = anTds[ ( i*oSettings.aoColumns.length) + (iCol*1) ];
					oSettings.aoData[i]._anHidden[iCol] = nTd;
					nTd.parentNode.removeChild( nTd );
				}
				
				oSettings.aoColumns[iCol].bVisible = false;
			}
			
			/* If there are any 'open' rows, then we need to alter the colspan for this col change */
			for ( i=0, iLen=oSettings.aoOpenRows.length ; i<iLen ; i++ )
			{
				oSettings.aoOpenRows[i].nTr.colSpan = _fnVisbleColumns( oSettings );
			}
			
			/* Do a redraw incase anything depending on the table columns needs it 
			 * (built-in: scrolling) 
			 */
			if ( typeof bRedraw == 'undefined' || bRedraw )
			{
				_fnAjustColumnSizing( oSettings );
				_fnDraw( oSettings );
			}
			
			_fnSaveState( oSettings );
		};
		
		/*
		 * Function: fnPageChange
		 * Purpose:  Change the pagination
		 * Returns:  -
		 * Inputs:   string:sAction - paging action to take: "first", "previous", "next" or "last"
		 *           bool:bRedraw - redraw the table or not - optional - default true
		 */
		this.fnPageChange = function ( sAction, bRedraw )
		{
			var oSettings = _fnSettingsFromNode( this[_oExt.iApiIndex] );
			_fnPageChange( oSettings, sAction );
			_fnCalculateEnd( oSettings );
			
			if ( typeof bRedraw == 'undefined' || bRedraw )
			{
				_fnDraw( oSettings );
			}
		};
		
		/*
		 * Function: fnDestroy
		 * Purpose:  Destructor for the DataTable
		 * Returns:  -
		 * Inputs:   -
		 */
		this.fnDestroy = function ( )
		{
			var oSettings = _fnSettingsFromNode( this[_oExt.iApiIndex] );
			var nOrig = oSettings.nTableWrapper.parentNode;
			var nBody = oSettings.nTBody;
			var i, iLen;
			
			/* Flag to note that the table is currently being destoryed - no action should be taken */
			oSettings.bDestroying = true;
			
			/* Restore hidden columns */
			for ( i=0, iLen=oSettings.aoColumns.length ; i<iLen ; i++ )
			{
				if ( oSettings.aoColumns[i].bVisible === false )
				{
					this.fnSetColumnVis( i, true );
				}
			}
			
			/* If there is an 'empty' indicator row, remove it */
			$('tbody>tr>td.'+oSettings.oClasses.sRowEmpty, oSettings.nTable).parent().remove();
			
			/* When scrolling we had to break the table up - restore it */
			if ( oSettings.nTable != oSettings.nTHead.parentNode )
			{
				$('>thead', oSettings.nTable).remove();
				oSettings.nTable.appendChild( oSettings.nTHead );
			}
			
			if ( oSettings.nTFoot && oSettings.nTable != oSettings.nTFoot.parentNode )
			{
				$('>tfoot', oSettings.nTable).remove();
				oSettings.nTable.appendChild( oSettings.nTFoot );
			}
			
			/* Remove the DataTables generated nodes, events and classes */
			oSettings.nTable.parentNode.removeChild( oSettings.nTable );
			$(oSettings.nTableWrapper).remove();
			
			oSettings.aaSorting = [];
			oSettings.aaSortingFixed = [];
			_fnSortingClasses( oSettings );
			
			$(_fnGetTrNodes( oSettings )).removeClass( oSettings.asStripClasses.join(' ') );
			
			if ( !oSettings.bJUI )
			{
				$('th', oSettings.nTHead).removeClass( [ _oExt.oStdClasses.sSortable,
					_oExt.oStdClasses.sSortableAsc,
					_oExt.oStdClasses.sSortableDesc,
					_oExt.oStdClasses.sSortableNone ].join(' ')
				);
			}
			else
			{
				$('th', oSettings.nTHead).removeClass( [ _oExt.oStdClasses.sSortable,
					_oExt.oJUIClasses.sSortableAsc,
					_oExt.oJUIClasses.sSortableDesc,
					_oExt.oJUIClasses.sSortableNone ].join(' ')
				);
				$('th span', oSettings.nTHead).remove();
			}
			
			/* Add the TR elements back into the table in their original order */
			nOrig.appendChild( oSettings.nTable );
			for ( i=0, iLen=oSettings.aoData.length ; i<iLen ; i++ )
			{
				nBody.appendChild( oSettings.aoData[i].nTr );
			}
			
			/* Restore the width of the original table */
			oSettings.nTable.style.width = _fnStringToCss(oSettings.sDestroyWidth);
			
			/* If the were originally odd/even type classes - then we add them back here. Note
			 * this is not fool proof (for example if not all rows as odd/even classes - but 
			 * it's a good effort without getting carried away
			 */
			$('>tr:even', nBody).addClass( oSettings.asDestoryStrips[0] );
			$('>tr:odd', nBody).addClass( oSettings.asDestoryStrips[1] );
			
			/* Remove the settings object from the settings array */
			for ( i=0, iLen=_aoSettings.length ; i<iLen ; i++ )
			{
				if ( _aoSettings[i] == oSettings )
				{
					_aoSettings.splice( i, 1 );
				}
			}
			
			/* End it all */
			oSettings = null;
		};
		
		/*
		 * Function: _fnAjustColumnSizing
		 * Purpose:  Update tale sizing based on content. This would most likely be used for scrolling
		 *   and will typically need a redraw after it.
		 * Returns:  -
		 * Inputs:   bool:bRedraw - redraw the table or not, you will typically want to - default true
		 */
		this.fnAdjustColumnSizing = function ( bRedraw )
		{
			_fnAjustColumnSizing( _fnSettingsFromNode( this[_oExt.iApiIndex] ) );
			
			if ( typeof bRedraw == 'undefined' || bRedraw )
			{
				this.fnDraw( false );
			}
		};
		
		/*
		 * Plugin API functions
		 * 
		 * This call will add the functions which are defined in _oExt.oApi to the
		 * DataTables object, providing a rather nice way to allow plug-in API functions. Note that
		 * this is done here, so that API function can actually override the built in API functions if
		 * required for a particular purpose.
		 */
		
		/*
		 * Function: _fnExternApiFunc
		 * Purpose:  Create a wrapper function for exporting an internal func to an external API func
		 * Returns:  function: - wrapped function
		 * Inputs:   string:sFunc - API function name
		 */
		function _fnExternApiFunc (sFunc)
		{
			return function() {
					var aArgs = [_fnSettingsFromNode(this[_oExt.iApiIndex])].concat( 
						Array.prototype.slice.call(arguments) );
					return _oExt.oApi[sFunc].apply( this, aArgs );
				};
		}
		
		for ( var sFunc in _oExt.oApi )
		{
			if ( sFunc )
			{
				/*
				 * Function: anon
				 * Purpose:  Wrap the plug-in API functions in order to provide the settings as 1st arg 
				 *   and execute in this scope
				 * Returns:  -
				 * Inputs:   -
				 */
				this[sFunc] = _fnExternApiFunc(sFunc);
			}
		}
		
		
		
		/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
		 * Section - Local functions
		 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
		
		/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
		 * Section - Initalisation
		 */
		
		/*
		 * Function: _fnInitalise
		 * Purpose:  Draw the table for the first time, adding all required features
		 * Returns:  -
		 * Inputs:   object:oSettings - dataTables settings object
		 */
		function _fnInitalise ( oSettings )
		{
			var i, iLen;
			
			/* Ensure that the table data is fully initialised */
			if ( oSettings.bInitialised === false )
			{
				setTimeout( function(){ _fnInitalise( oSettings ); }, 200 );
				return;
			}
			
			/* Show the display HTML options */
			_fnAddOptionsHtml( oSettings );
			
			/* Draw the headers for the table */
			_fnDrawHead( oSettings );
			
			/* Okay to show that something is going on now */
			_fnProcessingDisplay( oSettings, true );
			
			/* Calculate sizes for columns */
			if ( oSettings.oFeatures.bAutoWidth )
			{
				_fnCalculateColumnWidths( oSettings );
			}
			
			for ( i=0, iLen=oSettings.aoColumns.length ; i<iLen ; i++ )
			{
				if ( oSettings.aoColumns[i].sWidth !== null )
				{
					oSettings.aoColumns[i].nTh.style.width = _fnStringToCss( oSettings.aoColumns[i].sWidth );
				}
			}
			
			/* If there is default sorting required - let's do it. The sort function
			 * will do the drawing for us. Otherwise we draw the table
			 */
			if ( oSettings.oFeatures.bSort )
			{
				_fnSort( oSettings );
			}
			else
			{
				oSettings.aiDisplay = oSettings.aiDisplayMaster.slice();
				_fnCalculateEnd( oSettings );
				_fnDraw( oSettings );
			}
			
			/* if there is an ajax source */
			if ( oSettings.sAjaxSource !== null && !oSettings.oFeatures.bServerSide )
			{
				oSettings.fnServerData.call( oSettings.oInstance, oSettings.sAjaxSource, [], function(json) {
					/* Got the data - add it to the table */
					for ( i=0 ; i<json.aaData.length ; i++ )
					{
						_fnAddData( oSettings, json.aaData[i] );
					}
					
					/* Reset the init display for cookie saving. We've already done a filter, and
					 * therefore cleared it before. So we need to make it appear 'fresh'
					 */
					oSettings.iInitDisplayStart = oSettings._iDisplayStart;
					
					if ( oSettings.oFeatures.bSort )
					{
						_fnSort( oSettings );
					}
					else
					{
						oSettings.aiDisplay = oSettings.aiDisplayMaster.slice();
						_fnCalculateEnd( oSettings );
						_fnDraw( oSettings );
					}
					
					_fnProcessingDisplay( oSettings, false );
					
					/* Run the init callback if there is one - done here for ajax source for json obj */
					if ( typeof oSettings.fnInitComplete == 'function' )
					{
						oSettings.fnInitComplete.call( oSettings.oInstance, oSettings, json );
					}
				} );
				return;
			}
			
			if ( !oSettings.oFeatures.bServerSide )
			{
				_fnProcessingDisplay( oSettings, false );
			}
		}
		
		/*
		 * Function: _fnLanguageProcess
		 * Purpose:  Copy language variables from remote object to a local one
		 * Returns:  -
		 * Inputs:   object:oSettings - dataTables settings object
		 *           object:oLanguage - Language information
		 *           bool:bInit - init once complete
		 */
		function _fnLanguageProcess( oSettings, oLanguage, bInit )
		{
			_fnMap( oSettings.oLanguage, oLanguage, 'sProcessing' );
			_fnMap( oSettings.oLanguage, oLanguage, 'sLengthMenu' );
			_fnMap( oSettings.oLanguage, oLanguage, 'sEmptyTable' );
			_fnMap( oSettings.oLanguage, oLanguage, 'sZeroRecords' );
			_fnMap( oSettings.oLanguage, oLanguage, 'sInfo' );
			_fnMap( oSettings.oLanguage, oLanguage, 'sInfoEmpty' );
			_fnMap( oSettings.oLanguage, oLanguage, 'sInfoFiltered' );
			_fnMap( oSettings.oLanguage, oLanguage, 'sInfoPostFix' );
			_fnMap( oSettings.oLanguage, oLanguage, 'sSearch' );
			
			if ( typeof oLanguage.oPaginate != 'undefined' )
			{
				_fnMap( oSettings.oLanguage.oPaginate, oLanguage.oPaginate, 'sFirst' );
				_fnMap( oSettings.oLanguage.oPaginate, oLanguage.oPaginate, 'sPrevious' );
				_fnMap( oSettings.oLanguage.oPaginate, oLanguage.oPaginate, 'sNext' );
				_fnMap( oSettings.oLanguage.oPaginate, oLanguage.oPaginate, 'sLast' );
			}
			
			/* Backwards compatibility - if there is no sEmptyTable given, then use the same as
			 * sZeroRecords - assuming that is given.
			 */
			if ( typeof oLanguage.sEmptyTable == 'undefined' && 
			     typeof oLanguage.sZeroRecords != 'undefined' )
			{
				_fnMap( oSettings.oLanguage, oLanguage, 'sZeroRecords', 'sEmptyTable' );
			}
			
			if ( bInit )
			{
				_fnInitalise( oSettings );
			}
		}
		
		/*
		 * Function: _fnAddColumn
		 * Purpose:  Add a column to the list used for the table with default values
		 * Returns:  -
		 * Inputs:   object:oSettings - dataTables settings object
		 *           node:nTh - the th element for this column
		 */
		function _fnAddColumn( oSettings, nTh )
		{
			oSettings.aoColumns[ oSettings.aoColumns.length++ ] = {
				"sType": null,
				"_bAutoType": true,
				"bVisible": true,
				"bSearchable": true,
				"bSortable": true,
				"asSorting": [ 'asc', 'desc' ],
				"sSortingClass": oSettings.oClasses.sSortable,
				"sSortingClassJUI": oSettings.oClasses.sSortJUI,
				"sTitle": nTh ? nTh.innerHTML : '',
				"sName": '',
				"sWidth": null,
				"sWidthOrig": null,
				"sClass": null,
				"fnRender": null,
				"bUseRendered": true,
				"iDataSort": oSettings.aoColumns.length-1,
				"sSortDataType": 'std',
				"nTh": nTh ? nTh : document.createElement('th'),
				"nTf": null
			};
			
			var iCol = oSettings.aoColumns.length-1;
			var oCol = oSettings.aoColumns[ iCol ];
			
			/* Add a column specific filter */
			if ( typeof oSettings.aoPreSearchCols[ iCol ] == 'undefined' ||
			     oSettings.aoPreSearchCols[ iCol ] === null )
			{
				oSettings.aoPreSearchCols[ iCol ] = {
					"sSearch": "",
					"bRegex": false,
					"bSmart": true
				};
			}
			else
			{
				/* Don't require that the user must specify bRegex and / or bSmart */
				if ( typeof oSettings.aoPreSearchCols[ iCol ].bRegex == 'undefined' )
				{
					oSettings.aoPreSearchCols[ iCol ].bRegex = true;
				}
				
				if ( typeof oSettings.aoPreSearchCols[ iCol ].bSmart == 'undefined' )
				{
					oSettings.aoPreSearchCols[ iCol ].bSmart = true;
				}
			} 
			
			/* Use the column options function to initialise classes etc */
			_fnColumnOptions( oSettings, iCol, null );
		}
		
		/*
		 * Function: _fnColumnOptions
		 * Purpose:  Apply options for a column
		 * Returns:  -
		 * Inputs:   object:oSettings - data
Download .txt
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
Download .txt
SYMBOL INDEX (145 symbols across 10 files)

FILE: src/chrome_ex_oauth.js
  function ChromeExOAuth (line 21) | function ChromeExOAuth(url_request_token, url_auth_token, url_access_token,

FILE: src/chrome_ex_oauthsimple.js
  function _f (line 397) | function _f(t,b,c,d){if(t<20){return(b&c)|((~b)&d);}if(t<40){return b^c^...
  function _k (line 397) | function _k(t){return(t<20)?1518500249:(t<40)?1859775393:(t<60)?-1894007...
  function _s (line 397) | function _s(x,y){var l=(x&0xFFFF)+(y&0xFFFF),m=(x>>16)+(y>>16)+(l>>16);r...
  function _r (line 397) | function _r(n,c){return(n<<c)|(n>>>(32-c));}
  function _c (line 397) | function _c(x,l){x[l>>5]|=0x80<<(24-l%32);x[((l+64>>9)<<4)+15]=l;var w=[...
  function _b (line 397) | function _b(s){var b=[],m=(1<<_z)-1;for(var i=0;i<s.length*_z;i+=_z){b[i...
  function _h (line 397) | function _h(k,d){var b=_b(k);if(b.length>16){b=_c(b,k.length*_z);}var p=...
  function _n (line 397) | function _n(b){var t="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwx...
  function _x (line 397) | function _x(k,d){return _n(_h(k,d));}

FILE: src/js/shared.js
  function _parseParam (line 51) | function _parseParam(str) {

FILE: src/js/viewer.js
  function parseQueryString (line 352) | function parseQueryString(_1){var _2={};if(_1==undefined){_1=location.se...

FILE: src/lib/datatables-1.7.4/js/jquery.dataTables.js
  function classSettings (line 854) | function classSettings ()
  function _fnExternApiFunc (line 2212) | function _fnExternApiFunc (sFunc)
  function _fnInitalise (line 2252) | function _fnInitalise ( oSettings )
  function _fnLanguageProcess (line 2351) | function _fnLanguageProcess( oSettings, oLanguage, bInit )
  function _fnAddColumn (line 2393) | function _fnAddColumn( oSettings, nTh )
  function _fnColumnOptions (line 2456) | function _fnColumnOptions( oSettings, iCol, oOptions )
  function _fnAddData (line 2519) | function _fnAddData ( oSettings, aDataSupplied )
  function _fnGatherData (line 2628) | function _fnGatherData( oSettings )
  function _fnDrawHead (line 2791) | function _fnDrawHead( oSettings )
  function _fnDraw (line 2915) | function _fnDraw( oSettings )
  function _fnReDraw (line 3122) | function _fnReDraw( oSettings )
  function _fnAjaxUpdate (line 3147) | function _fnAjaxUpdate( oSettings )
  function _fnAjaxUpdateDraw (line 3233) | function _fnAjaxUpdateDraw ( oSettings, json )
  function _fnAddOptionsHtml (line 3303) | function _fnAddOptionsHtml ( oSettings )
  function _fnFeatureHtmlTable (line 3471) | function _fnFeatureHtmlTable ( oSettings )
  function _fnScrollDraw (line 3633) | function _fnScrollDraw ( o )
  function _fnAjustColumnSizing (line 3880) | function _fnAjustColumnSizing ( oSettings )
  function _fnFeatureHtmlFilter (line 3906) | function _fnFeatureHtmlFilter ( oSettings )
  function _fnFilterComplete (line 3960) | function _fnFilterComplete ( oSettings, oInput, iForce )
  function _fnFilterCustom (line 3996) | function _fnFilterCustom( oSettings )
  function _fnFilterColumn (line 4026) | function _fnFilterColumn ( oSettings, sInput, iColumn, bRegex, bSmart )
  function _fnFilter (line 4058) | function _fnFilter( oSettings, sInput, iForce, bRegex, bSmart )
  function _fnBuildSearchArray (line 4141) | function _fnBuildSearchArray ( oSettings, iMaster )
  function _fnBuildSearchRow (line 4163) | function _fnBuildSearchRow( oSettings, aData )
  function _fnFilterCreateSearch (line 4198) | function _fnFilterCreateSearch( sSearch, bRegex, bSmart )
  function _fnDataToSearch (line 4225) | function _fnDataToSearch ( sData, sType )
  function _fnSort (line 4258) | function _fnSort ( oSettings, bApplyClasses )
  function _fnSortAttachListener (line 4436) | function _fnSortAttachListener ( oSettings, nNode, iDataIndex, fnCallback )
  function _fnSortingClasses (line 4553) | function _fnSortingClasses( oSettings )
  function _fnFeatureHtmlPaginate (line 4706) | function _fnFeatureHtmlPaginate ( oSettings )
  function _fnPageChange (line 4746) | function _fnPageChange ( oSettings, sAction )
  function _fnFeatureHtmlInfo (line 4812) | function _fnFeatureHtmlInfo ( oSettings )
  function _fnUpdateInfo (line 4842) | function _fnUpdateInfo ( oSettings )
  function _fnFeatureHtmlLength (line 4922) | function _fnFeatureHtmlLength ( oSettings )
  function _fnFeatureHtmlProcessing (line 5016) | function _fnFeatureHtmlProcessing ( oSettings )
  function _fnProcessingDisplay (line 5040) | function _fnProcessingDisplay ( oSettings, bShow )
  function _fnVisibleToColumnIndex (line 5064) | function _fnVisibleToColumnIndex( oSettings, iMatch )
  function _fnColumnIndexToVisible (line 5091) | function _fnColumnIndexToVisible( oSettings, iMatch )
  function _fnNodeToDataIndex (line 5118) | function _fnNodeToDataIndex( s, n )
  function _fnVisbleColumns (line 5150) | function _fnVisbleColumns( oS )
  function _fnCalculateEnd (line 5169) | function _fnCalculateEnd( oSettings )
  function _fnConvertToWidth (line 5200) | function _fnConvertToWidth ( sWidth, nParent )
  function _fnCalculateColumnWidths (line 5229) | function _fnCalculateColumnWidths ( oSettings )
  function _fnScrollingWidthAdjust (line 5397) | function _fnScrollingWidthAdjust ( oSettings, n )
  function _fnGetWidestNode (line 5427) | function _fnGetWidestNode( oSettings, iCol, bFast )
  function _fnGetMaxLenString (line 5484) | function _fnGetMaxLenString( oSettings, iCol )
  function _fnStringToCss (line 5509) | function _fnStringToCss( s )
  function _fnArrayCmp (line 5541) | function _fnArrayCmp( aArray1, aArray2 )
  function _fnDetectType (line 5567) | function _fnDetectType( sData )
  function _fnSettingsFromNode (line 5590) | function _fnSettingsFromNode ( nTable )
  function _fnGetDataMaster (line 5609) | function _fnGetDataMaster ( oSettings )
  function _fnGetTrNodes (line 5626) | function _fnGetTrNodes ( oSettings )
  function _fnGetTdNodes (line 5643) | function _fnGetTdNodes ( oSettings )
  function _fnEscapeRegex (line 5686) | function _fnEscapeRegex ( sVal )
  function _fnDeleteIndex (line 5701) | function _fnDeleteIndex( a, iTarget )
  function _fnReOrderIndex (line 5729) | function _fnReOrderIndex ( oSettings, sColumns )
  function _fnColumnOrdering (line 5755) | function _fnColumnOrdering ( oSettings )
  function _fnLog (line 5776) | function _fnLog( oSettings, iLevel, sMesg )
  function _fnClearTable (line 5806) | function _fnClearTable( oSettings )
  function _fnSaveState (line 5820) | function _fnSaveState ( oSettings )
  function _fnLoadState (line 5885) | function _fnLoadState ( oSettings, oInit )
  function _fnCreateCookie (line 5980) | function _fnCreateCookie ( sName, sValue, iSecs, sBaseName, fnCallback )
  function _fnReadCookie (line 6051) | function _fnReadCookie ( sName )
  function _fnGetUniqueThs (line 6081) | function _fnGetUniqueThs ( nThead )
  function _fnScrollBarWidth (line 6177) | function _fnScrollBarWidth ()
  function _fnApplyToChildren (line 6217) | function _fnApplyToChildren( fn, an1, an2 )
  function _fnMap (line 6247) | function _fnMap( oRet, oSrc, sName, sMappedName )

FILE: src/lib/jquery-ui-1.8.6/js/jquery-1.4.2.js
  function doScrollCheck (line 759) | function doScrollCheck() {
  function evalScript (line 777) | function evalScript( i, elem ) {
  function access (line 795) | function access( elems, key, value, exec, fn, pass ) {
  function now (line 822) | function now() {
  function returnFalse (line 2099) | function returnFalse() {
  function returnTrue (line 2102) | function returnTrue() {
  function trigger (line 2336) | function trigger( type, elem, args ) {
  function handler (line 2353) | function handler( e ) {
  function liveHandler (line 2528) | function liveHandler( event ) {
  function liveConvert (line 2590) | function liveConvert( type, selector ) {
  function getText (line 3419) | function getText( elems ) {
  function dirNodeCheck (line 3573) | function dirNodeCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) {
  function dirCheck (line 3604) | function dirCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) {
  function isDisconnected (line 3849) | function isDisconnected( node ) {
  function root (line 4320) | function root( elem, cur ) {
  function cloneCopyEvent (line 4329) | function cloneCopyEvent(orig, ret) {
  function buildFragment (line 4352) | function buildFragment( args, nodes, scripts ) {
  function getWH (line 4628) | function getWH() {
  function success (line 5264) | function success() {
  function complete (line 5276) | function complete() {
  function trigger (line 5293) | function trigger(type, args) {
  function buildParams (line 5404) | function buildParams( prefix, obj ) {
  function add (line 5435) | function add( key, value ) {
  function t (line 5762) | function t( gotoEnd ) {
  function genFx (line 5915) | function genFx( type, num ) {
  function getWindow (line 6173) | function getWindow( elem ) {

FILE: src/lib/jquery-ui-1.8.6/js/jquery-ui-1.8.6.highlight.js
  function getRGB (line 43) | function getRGB(color) {
  function getColor (line 74) | function getColor(elem, attr) {
  function getElementStyles (line 160) | function getElementStyles() {
  function filterStyles (line 191) | function filterStyles(styles) {
  function styleDifference (line 215) | function styleDifference(oldStyle, newStyle) {
  function _normalizeArguments (line 408) | function _normalizeArguments(effect, options, speed, callback) {
  function standardSpeed (line 442) | function standardSpeed( speed ) {

FILE: src/lib/jquery-ui-1.8.6/js/jquery-ui-1.8.6.js
  function reduce (line 142) | function reduce( elem, size, border, margin ) {
  function visible (line 177) | function visible( element ) {
  function filteredUi (line 6148) | function filteredUi(ui) {
  function filteredUi (line 6189) | function filteredUi(ui) {
  function getNextTabId (line 7299) | function getNextTabId() {
  function getNextListId (line 7303) | function getNextListId() {
  function resetStyle (line 7576) | function resetStyle( $el, fx ) {
  function Datepicker (line 8063) | function Datepicker() {
  function extendRemove (line 9719) | function extendRemove(target, props) {
  function isArray (line 9728) | function isArray(a) {
  function getRGB (line 9910) | function getRGB(color) {
  function getColor (line 9941) | function getColor(elem, attr) {
  function getElementStyles (line 10027) | function getElementStyles() {
  function filterStyles (line 10058) | function filterStyles(styles) {
  function styleDifference (line 10082) | function styleDifference(oldStyle, newStyle) {
  function _normalizeArguments (line 10275) | function _normalizeArguments(effect, options, speed, callback) {
  function standardSpeed (line 10309) | function standardSpeed( speed ) {

FILE: src/lib/jquery.layout-1.2.0.js
  function bindCallback (line 397) | function bindCallback (p, test) {
  function close_2 (line 1536) | function close_2 () {
  function open_2 (line 1628) | function open_2 () {
  function cancelMouseOut (line 1786) | function cancelMouseOut (evt) {
  function close_NOW (line 1815) | function close_NOW () {
  function keyDown (line 2138) | function keyDown (evt) {
  function allowOverflow (line 2204) | function allowOverflow (elem) {
  function resetOverflow (line 2265) | function resetOverflow (elem) {
  function getBtn (line 2302) | function getBtn(selector, pane, action) {
  function addToggleBtn (line 2328) | function addToggleBtn (selector, pane) {
  function addOpenBtn (line 2348) | function addOpenBtn (selector, pane) {
  function addCloseBtn (line 2368) | function addCloseBtn (selector, pane) {
  function addPinBtn (line 2395) | function addPinBtn (selector, pane) {
  function syncPinBtns (line 2423) | function syncPinBtns (pane, doPin) {
  function setPinState (line 2439) | function setPinState ($Pin, pane, doPin) {

FILE: src/test/lib/jasmine-1.0.1/jasmine.js
  function getGlobal (line 35) | function getGlobal() {
Condensed preview — 45 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (1,248K chars).
[
  {
    "path": ".gitignore",
    "chars": 85,
    "preview": "syntax:glob\nbuild\n*.pem\n.DS_Store\ntarget\nIcon?\nehthumbs.db\nThumbs.db\n*.crx\n*.zip\npkg\n"
  },
  {
    "path": "LICENSE.txt",
    "chars": 1513,
    "preview": "Copyright (c) 2010, David Heaton\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or withou"
  },
  {
    "path": "README.md",
    "chars": 2323,
    "preview": "Scraper\n=======\n\nA Google Chrome extension for getting data out of web pages and into spreadsheets.\n\nUsage\n-----\n\nHighli"
  },
  {
    "path": "Rakefile",
    "chars": 4107,
    "preview": "# Copyright (c) 2010, David Heaton\n# All rights reserved.\n# \n# Redistribution and use in source and binary forms, with o"
  },
  {
    "path": "src/background.html",
    "chars": 2082,
    "preview": "<!DOCTYPE html>\n\n<!-- \nCopyright (c) 2010, David Heaton\nAll rights reserved.\n\nRedistribution and use in source and binar"
  },
  {
    "path": "src/chrome_ex_oauth.html",
    "chars": 707,
    "preview": "<!DOCTYPE html>\n<!--\n * Copyright (c) 2009 The Chromium Authors. All rights reserved.  Use of this\n * source code is gov"
  },
  {
    "path": "src/chrome_ex_oauth.js",
    "chars": 22453,
    "preview": "/**\n * Copyright (c) 2010 The Chromium Authors. All rights reserved.  Use of this\n * source code is governed by a BSD-st"
  },
  {
    "path": "src/chrome_ex_oauthsimple.js",
    "chars": 19629,
    "preview": "/* OAuthSimple\n  * A simpler version of OAuth\n  *\n  * author:     jr conlin\n  * mail:       src@anticipatr.com\n  * copyr"
  },
  {
    "path": "src/css/base.css",
    "chars": 1704,
    "preview": "html, body, div, span, applet, object, iframe,\nh1, h2, h3, h4, h5, h6, p, blockquote, pre,\na, abbr, acronym, address, bi"
  },
  {
    "path": "src/css/popup.css",
    "chars": 832,
    "preview": "@import url('base.css');\n\nhtml {\n  width: 300px;\n}\n\nbody {\n  margin: 10px;\n  padding: 0;\n}\n\n#presets {\n  margin: 0;\n  ov"
  },
  {
    "path": "src/css/viewer.css",
    "chars": 11447,
    "preview": "@import url('base.css');\n\nbody {\n  font-family: \"Lucida Grande\",Arial,sans-serif;\n  font-size: 10px;\n  margin: 0;\n  padd"
  },
  {
    "path": "src/js/background.js",
    "chars": 5826,
    "preview": "/*\n * background.js\n *\n * Author: dave@bit155.com\n *\n * ----------------------------------------------------------------"
  },
  {
    "path": "src/js/bit155/attr.js",
    "chars": 3702,
    "preview": "/*\n * attr.js\n *\n * Author: dave@bit155.com\n *\n * ----------------------------------------------------------------------"
  },
  {
    "path": "src/js/bit155/csv.js",
    "chars": 2947,
    "preview": "/*\n * csv.js\n *\n * Author: dave@bit155.com\n *\n * -----------------------------------------------------------------------"
  },
  {
    "path": "src/js/bit155/scraper.js",
    "chars": 11085,
    "preview": "/*\n * scraper.js\n *\n * Author: dave@bit155.com\n *\n * -------------------------------------------------------------------"
  },
  {
    "path": "src/js/contentscript.js",
    "chars": 4465,
    "preview": "/*\n * contentscript.js\n *\n * Author: dave@bit155.com\n *\n * -------------------------------------------------------------"
  },
  {
    "path": "src/js/popup.js",
    "chars": 2282,
    "preview": "/*\n * popup.js\n *\n * Author: dave@bit155.com\n *\n * ---------------------------------------------------------------------"
  },
  {
    "path": "src/js/shared.js",
    "chars": 7674,
    "preview": "/*\n * shared.js\n *\n * Author: dave@bit155.com\n *\n * --------------------------------------------------------------------"
  },
  {
    "path": "src/js/viewer.js",
    "chars": 18042,
    "preview": "/*\n * viewer.js\n *\n * Author: dave@bit155.com\n *\n * --------------------------------------------------------------------"
  },
  {
    "path": "src/lib/datatables-1.7.4/js/jquery.dataTables.js",
    "chars": 210387,
    "preview": "/*\n * File:        jquery.dataTables.js\n * Version:     1.7.4\n * Description: Paginate, search and sort HTML tables\n * A"
  },
  {
    "path": "src/lib/jquery-ui-1.8.6/css/custom-theme/jquery-ui-1.8.6.custom.css",
    "chars": 32564,
    "preview": "/*\n * jQuery UI CSS Framework 1.8.6\n *\n * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about)\n * Dual licensed under"
  },
  {
    "path": "src/lib/jquery-ui-1.8.6/js/jquery-1.4.2.js",
    "chars": 163854,
    "preview": "/*!\n * jQuery JavaScript Library v1.4.2\n * http://jquery.com/\n *\n * Copyright 2010, John Resig\n * Dual licensed under th"
  },
  {
    "path": "src/lib/jquery-ui-1.8.6/js/jquery-ui-1.8.6.highlight.js",
    "chars": 23957,
    "preview": "/*\n * jQuery UI Effects 1.8.6\n *\n * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about)\n * Dual licensed under the M"
  },
  {
    "path": "src/lib/jquery-ui-1.8.6/js/jquery-ui-1.8.6.js",
    "chars": 363275,
    "preview": "/*!\n * jQuery UI 1.8.6\n *\n * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about)\n * Dual licensed under the MIT or G"
  },
  {
    "path": "src/lib/jquery.layout-1.2.0.js",
    "chars": 82920,
    "preview": "/*\r\n * jquery.layout 1.2.0\r\n *\r\n * Copyright (c) 2008 \r\n *   Fabrizio Balliano (http://www.fabrizioballiano.net)\r\n *   K"
  },
  {
    "path": "src/lib/jquery.tablednd_0_5.js",
    "chars": 16664,
    "preview": "/**\n * TableDnD plug-in for JQuery, allows you to drag and drop table rows\n * You can set up various options to control "
  },
  {
    "path": "src/license.html",
    "chars": 1741,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <title>Scraper License</title>\n    <style type=\"text/css\" media=\"screen\">\n      body"
  },
  {
    "path": "src/manifest.json",
    "chars": 778,
    "preview": "{\n  \"name\": \"Scraper\",\n  \"version\": \"1.6\",\n  \"description\": \"Scraper is a Google Chrome extension for getting data out o"
  },
  {
    "path": "src/popup.html",
    "chars": 2403,
    "preview": "<!DOCTYPE html>\n<!-- Copyright (c) 2010, David Heaton\nAll rights reserved.\n\nRedistribution and use in source and binary "
  },
  {
    "path": "src/test/SpecRunner.html",
    "chars": 3002,
    "preview": "<!DOCTYPE html>\n<!-- Copyright (c) 2010, David Heaton\nAll rights reserved.\n\nRedistribution and use in source and binary "
  },
  {
    "path": "src/test/lib/jasmine-1.0.1/MIT.LICENSE",
    "chars": 1061,
    "preview": "Copyright (c) 2008-2010 Pivotal Labs\n\nPermission is hereby granted, free of charge, to any person obtaining\na copy of th"
  },
  {
    "path": "src/test/lib/jasmine-1.0.1/jasmine-html.js",
    "chars": 6967,
    "preview": "jasmine.TrivialReporter = function(doc) {\n  this.document = doc || document;\n  this.suiteDivs = {};\n  this.logRunningSpe"
  },
  {
    "path": "src/test/lib/jasmine-1.0.1/jasmine.css",
    "chars": 2120,
    "preview": "body {\n  font-family: \"Helvetica Neue Light\", \"Lucida Grande\", \"Calibri\", \"Arial\", sans-serif;\n}\n\n\n.jasmine_reporter a:v"
  },
  {
    "path": "src/test/lib/jasmine-1.0.1/jasmine.js",
    "chars": 64942,
    "preview": "/**\n * Top level namespace for Jasmine, a lightweight JavaScript BDD/spec/testing framework.\n *\n * @namespace\n */\nvar ja"
  },
  {
    "path": "src/test/spec/bit155/attr.spec.js",
    "chars": 4737,
    "preview": "/*\n * attr.spec.js\n *\n * Author: dave@bit155.com\n *\n * -----------------------------------------------------------------"
  },
  {
    "path": "src/test/spec/bit155/csv.spec.js",
    "chars": 4388,
    "preview": "/*\n * csv.spec.js\n *\n * Author: dave@bit155.com\n *\n * ------------------------------------------------------------------"
  },
  {
    "path": "src/test/spec/bit155/scraper.spec.js",
    "chars": 1825,
    "preview": "/*\n * scraper.spec.js\n *\n * Author: dave@bit155.com\n *\n * --------------------------------------------------------------"
  },
  {
    "path": "src/test/spec/jquery-commonAncestor.spec.js",
    "chars": 3370,
    "preview": "/*\n * jquery-commonAncestor.spec.js\n *\n * Author: dave@bit155.com\n *\n * ------------------------------------------------"
  },
  {
    "path": "src/test/spec/jquery-serializeParams.spec.js",
    "chars": 3273,
    "preview": "/*\n * jquery-serializeParams.spec.js\n *\n * Author: dave@bit155.com\n *\n * -----------------------------------------------"
  },
  {
    "path": "src/test/spec/jquery-xpath.spec.js",
    "chars": 2259,
    "preview": "/*\n * jquery-serializeParams.spec.js\n *\n * Author: dave@bit155.com\n *\n * -----------------------------------------------"
  },
  {
    "path": "src/viewer.html",
    "chars": 6676,
    "preview": "<!DOCTYPE html>\n<!-- Copyright (c) 2010, David Heaton\nAll rights reserved.\n\nRedistribution and use in source and binary "
  }
]

// ... and 4 more files (download for full content)

About this extraction

This page contains the full source code of the mnmldave/scraper GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 45 files (1.1 MB), approximately 315.8k tokens, and a symbol index with 145 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!