Craft a single web page where I can test this library very easily and thoroughly. Craft a detailed document with examples and do and do not and templates for this library. /*!
 * ScraperLib.js v1.0.0
 * A highly customizable, client-side web scraping library.
 * Author: OpenAI Assistant
 * License: MIT
 */

(function (global) {

  'use strict';

  class ScraperLib {

    /**
     * Initializes the ScraperLib with global settings.
     * @param {Object} globalOptions - The global configuration options.
     */
    constructor(globalOptions = {}) {
      // Default global settings
      this.settings = {
        useStrictParsing: false,           // Enable strict parsing
        fetchConcurrently: true,           // Fetch all URLs concurrently
        cacheEnabled: false,               // Enable caching
        userAgents: {},                    // Custom user agents per URL
        defaultUserAgent: navigator.userAgent, // Default user agent
        modifiedSince: {},                 // 'If-Modified-Since' headers per URL
        proxies: {},                       // Proxy URLs per target URL
        enableProxy: {},                   // Enable proxy per URL
        corsProxies: {},                   // CORS proxy URLs per target URL
        enableCorsProxy: {},               // Enable CORS proxy per URL
        headers: {},                       // Custom headers per URL
        outputFormat: 'json',              // Output format: 'json', 'dom', etc.
        db: null,                          // IndexedDB instance if needed
        useStreaming: false,               // Stream data as it's extracted
        onData: null,                      // Callback for streaming data
        onComplete: null,                  // Callback when all data is fetched
        ...globalOptions                   // Override defaults with user options
      };

      // Validate database settings
      if ((this.settings.outputFormat === 'indexdb' || this.settings.outputFormat === 'dexie') && !this.settings.db) {
        throw new Error(`Database instance is required for output format '${this.settings.outputFormat}'`);
      }
    }

    /**
     * Scrapes multiple URLs based on the provided configuration.
     * @param {Object} urlConfigs - An object where keys are URLs and values are their configurations.
     * @returns {Promise<Object>} - A promise that resolves to an object containing the scraped data.
     */
    async scrape(urlConfigs) {
      const results = {};
      const tasks = [];

      for (const url in urlConfigs) {
        const config = urlConfigs[url];
        const task = this._fetchAndProcess(url, config)
          .then(data => {
            results[url] = data;
            // Stream data if enabled
            if (this.settings.useStreaming && typeof this.settings.onData === 'function') {
              this.settings.onData(url, data);
            }
          })
          .catch(err => {
            results[url] = { error: err.message };
          });
        tasks.push(task);
      }

      if (this.settings.fetchConcurrently) {
        await Promise.all(tasks);
      } else {
        for (const task of tasks) {
          await task;
        }
      }

      // Invoke completion callback
      if (typeof this.settings.onComplete === 'function') {
        this.settings.onComplete(results);
      }

      // Return results if not streaming
      if (!this.settings.useStreaming) {
        return results;
      }
    }

    /**
     * Fetches and processes a single URL.
     * @private
     * @param {string} url - The URL to fetch.
     * @param {Object} config - The configuration for this URL.
     * @returns {Promise<Object>} - A promise that resolves to the extracted data.
     */
    async _fetchAndProcess(url, config) {
      // Validate URL and selectors
      if (!url) {
        throw new Error('URL is required.');
      }
      if (!config || !config.selectors) {
        throw new Error(`Selectors are required for URL: ${url}`);
      }

      const finalUrl = this._buildUrl(url, config);
      const options = this._buildFetchOptions(url, config);

      let response;
      try {
        response = await fetch(finalUrl, options);
      } catch (error) {
        throw new Error(`Failed to fetch '${url}': ${error.message}`);
      }

      if (!response.ok) {
        throw new Error(`Failed to fetch '${url}', status: ${response.status}`);
      }

      let text;
      try {
        text = await response.text();
      } catch (error) {
        throw new Error(`Failed to read response from '${url}': ${error.message}`);
      }

      // Remove scripts and styles to prevent XSS and reduce parsing overhead
      text = text.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '');
      text = text.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '');

      // Parse HTML
      let doc;
      try {
        const parser = new DOMParser();
        doc = parser.parseFromString(text, 'text/html');
      } catch (error) {
        throw new Error(`Failed to parse HTML from '${url}': ${error.message}`);
      }

      // Extract data using CSS selectors
      const data = {};
      for (const key in config.selectors) {
        const selector = config.selectors[key];
        if (!selector) {
          data[key] = [];
          continue;
        }
        const elements = doc.querySelectorAll(selector);
        data[key] = [];
        elements.forEach(element => {
          data[key].push(element.textContent.trim());
        });
      }

      // Output data based on user settings
      await this._outputData(url, data);

      // Clean up
      doc = null;

      return data;
    }

    /**
     * Builds the final URL with proxy if enabled.
     * @private
     * @param {string} url - The original URL.
     * @param {Object} config - The configuration for this URL.
     * @returns {string} - The final URL to fetch.
     */
    _buildUrl(url, config) {
      let finalUrl = url;

      // Check if proxy is enabled for this URL
      const enableProxy = config.enableProxy !== undefined ? config.enableProxy
        : this.settings.enableProxy[url] !== undefined ? this.settings.enableProxy[url]
        : false;

      if (enableProxy) {
        const proxyUrl = config.proxyUrl || this.settings.proxies[url] || this.settings.defaultProxy;
        if (proxyUrl) {
          finalUrl = proxyUrl + encodeURIComponent(url);
        }
      }

      // Check if CORS proxy is enabled
      const enableCorsProxy = config.enableCorsProxy !== undefined ? config.enableCorsProxy
        : this.settings.enableCorsProxy[url] !== undefined ? this.settings.enableCorsProxy[url]
        : false;

      if (enableCorsProxy) {
        const corsProxyUrl = config.corsProxyUrl || this.settings.corsProxies[url];
        if (corsProxyUrl) {
          finalUrl = corsProxyUrl + encodeURIComponent(finalUrl);
        }
      }

      return finalUrl;
    }

    /**
     * Builds the fetch options including headers.
     * @private
     * @param {string} url - The URL to fetch.
     * @param {Object} config - The configuration for this URL.
     * @returns {Object} - The fetch options.
     */
    _buildFetchOptions(url, config) {
      const headers = new Headers();

      // Set User-Agent
      const userAgent = config.userAgent || this.settings.userAgents[url] || this.settings.defaultUserAgent;
      if (userAgent) {
        headers.append('User-Agent', userAgent);
      }

      // Set If-Modified-Since header
      const modifiedSince = config.modifiedSince || this.settings.modifiedSince[url];
      if (modifiedSince) {
        headers.append('If-Modified-Since', modifiedSince);
      }

      // Set custom headers
      const customHeaders = config.headers || this.settings.headers[url] || {};
      for (const headerName in customHeaders) {
        headers.append(headerName, customHeaders[headerName]);
      }

      const options = {
        method: 'GET',
        headers: headers,
        mode: 'cors',
        cache: this.settings.cacheEnabled ? 'default' : 'no-cache',
        redirect: 'follow',
      };

      return options;
    }

    /**
     * Outputs the extracted data based on settings.
     * @private
     * @param {string} url - The URL that was processed.
     * @param {Object} data - The extracted data.
     */
    async _outputData(url, data) {
      switch (this.settings.outputFormat) {
        case 'json':
          // Data is already in JSON format
          break;
        case 'dom':
          // Not practical to return DOM in this context
          break;
        case 'indexdb':
          await this._storeInIndexedDB(url, data);
          break;
        case 'dexie':
          await this._storeInDexie(url, data);
          break;
        case 'log':
          console.log(`Data from '${url}':`, data);
          break;
        default:
          // Default to JSON, do nothing
          break;
      }
    }

    /**
     * Stores data in IndexedDB.
     * @private
     * @param {string} url - The URL that was processed.
     * @param {Object} data - The extracted data.
     * @returns {Promise<void>}
     */
    async _storeInIndexedDB(url, data) {
      const db = this.settings.db;
      const transaction = db.transaction(['scrapedData'], 'readwrite');
      const store = transaction.objectStore('scrapedData');
      return new Promise((resolve, reject) => {
        const request = store.put({ url: url, data: data, timestamp: Date.now() });
        request.onsuccess = () => resolve();
        request.onerror = e => reject(e);
      });
    }

    /**
     * Stores data in a Dexie database.
     * @private
     * @param {string} url - The URL that was processed.
     * @param {Object} data - The extracted data.
     */
    async _storeInDexie(url, data) {
      const db = this.settings.db;
      try {
        await db.scrapedData.put({ url: url, data: data, timestamp: Date.now() });
      } catch (error) {
        throw new Error(`Failed to store data in Dexie: ${error.message}`);
      }
    }

  }

  // Make the library globally available
  global.ScraperLib = ScraperLib;

})(typeof window !== 'undefined' ? window : this);