Craft a working single web page where I can test this library very easily and thoroughly.  Also complete this library and make this actually work. send the fully working full codes. --- /*!
 * ScraperLib.js v2.0.0
 * A highly customizable, client-side web scraping library.
 * Author: OpenAI Assistant
 * License: MIT
 *
 * Features:
 * - Highly customizable settings globally and per URL
 * - Supports multiple URLs with individual CSS selectors and proxy configurations
 * - Handles errors, retries, and edge cases gracefully
 * - Outputs data in JSON, DOM, IndexedDB, Dexie, or logs
 * - Supports streaming of extracted data via callbacks
 * - Efficient memory management and resource cleanup
 * - Prevents XSS and other security vulnerabilities
 * - Easy to use with comprehensive documentation
 */

(function (global) {

  'use strict';

  /**
   * Utility function to validate URLs.
   * @param {string} url - The URL to validate.
   * @returns {boolean} - True if valid, false otherwise.
   */
  function isValidURL(url) {
    try {
      new URL(url);
      return true;
    } catch (e) {
      return false;
    }
  }

  /**
   * Utility function to sanitize strings to prevent XSS attacks.
   * @param {string} str - The string to sanitize.
   * @returns {string} - The sanitized string.
   */
  function sanitize(str) {
    const div = document.createElement('div');
    div.textContent = str;
    return div.innerHTML;
  }

  /**
   * Utility function to debounce execution.
   * @param {Function} func - The function to debounce.
   * @param {number} wait - Delay in milliseconds.
   * @returns {Function} - The debounced function.
   */
  function debounce(func, wait) {
    let timeout;
    return function (...args) {
      clearTimeout(timeout);
      timeout = setTimeout(() => func.apply(this, args), wait);
    };
  }

  /**
   * Initializes IndexedDB for storing scraped data.
   * @param {string} dbName - Name of the database.
   * @param {string} storeName - Name of the object store.
   * @returns {Promise<IDBDatabase>} - Promise resolving to the database instance.
   */
  function initIndexedDB(dbName = 'ScraperLibDB', storeName = 'scrapedData') {
    return new Promise((resolve, reject) => {
      const request = indexedDB.open(dbName, 1);
      request.onerror = () => reject(new Error('Failed to open IndexedDB.'));
      request.onupgradeneeded = (event) => {
        const db = event.target.result;
        if (!db.objectStoreNames.contains(storeName)) {
          db.createObjectStore(storeName, { keyPath: 'url' });
        }
      };
      request.onsuccess = (event) => resolve(event.target.result);
    });
  }

  /**
   * ScraperLib Class
   */
  class ScraperLib {

    /**
     * Initializes the ScraperLib with global settings.
     * @param {Object} globalOptions - 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', 'indexdb', 'dexie', 'log'
        db: null,                                       // IndexedDB or Dexie 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
        timeout: 15000,                                // Fetch timeout in milliseconds
        retries: 3,                                    // Number of retries for failed fetches
        retryDelay: 1000,                              // Delay between retries in milliseconds
        ...globalOptions                               // Override defaults with user options
      };

      // Initialize IndexedDB or Dexie if required
      if (['indexdb', 'dexie'].includes(this.settings.outputFormat.toLowerCase())) {
        if (!this.settings.db) {
          throw new Error(`Database instance is required for output format '${this.settings.outputFormat}'`);
        }
      }

      // Bind methods
      this.scrape = this.scrape.bind(this);
      this._fetchWithRetries = this._fetchWithRetries.bind(this);
      this._handleFetchError = this._handleFetchError.bind(this);
      this._debouncedCleanUp = debounce(this._cleanUp.bind(this), 5000);

      // Storage for active fetches to prevent memory leaks
      this.activeFetches = new Set();
    }

    /**
     * Scrapes multiple URLs based on the provided configuration.
     * @param {Object} urlConfigs - Object where keys are URLs and values are their configurations.
     * @returns {Promise<Object>} - Promise resolving to an object containing the scraped data.
     */
    async scrape(urlConfigs) {
      if (typeof urlConfigs !== 'object' || urlConfigs === null) {
        throw new Error('urlConfigs must be a non-null object.');
      }

      const results = {};
      const tasks = [];

      for (const url in urlConfigs) {
        const config = urlConfigs[url];

        // Validate URL
        if (!isValidURL(url)) {
          results[url] = { error: 'Invalid URL.' };
          continue;
        }

        // Validate selectors
        if (!config || typeof config.selectors !== 'object' || config.selectors === null) {
          results[url] = { error: 'Selectors are required and must be an object.' };
          continue;
        }

        // Prepare the fetch task
        const task = this._fetchAndProcess(url, config)
          .then(data => {
            results[url] = data;
            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;
        }
      }

      if (typeof this.settings.onComplete === 'function') {
        this.settings.onComplete(results);
      }

      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>} - Promise resolving to the extracted data.
     */
    async _fetchAndProcess(url, config) {
      const finalUrl = this._buildUrl(url, config);
      const options = this._buildFetchOptions(url, config);

      // Setup abort controller for timeout
      const controller = new AbortController();
      const timeoutId = setTimeout(() => controller.abort(), this.settings.timeout);
      options.signal = controller.signal;

      // Track active fetch
      const fetchPromise = this._fetchWithRetries(finalUrl, options, this.settings.retries)
        .finally(() => {
          clearTimeout(timeoutId);
          this.activeFetches.delete(fetchPromise);
          this._debouncedCleanUp();
        });

      this.activeFetches.add(fetchPromise);

      let response;
      try {
        response = await fetchPromise;
      } 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(sanitize(element.textContent.trim()));
        });
      }

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

      // Clean up
      doc = null;
      text = null;

      return data;
    }

    /**
     * Builds the final URL with proxy and CORS 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.proxies['default'];
        if (proxyUrl) {
          finalUrl = proxyUrl + encodeURIComponent(finalUrl);
        }
      }

      // 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] || this.settings.corsProxies['default'];
        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.toLowerCase()) {
        case 'json':
          // Data is already in JSON format
          break;
        case 'dom':
          // Convert JSON to DOM elements (Example: Creating a preformatted text node)
          // Users can extend this as needed
          const pre = document.createElement('pre');
          pre.textContent = JSON.stringify(data, null, 2);
          document.body.appendChild(pre);
          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) {
      if (!this.settings.db) {
        throw new Error('IndexedDB instance is not initialized.');
      }
      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 = () => reject(new Error('Failed to store data in IndexedDB.'));
      });
    }

    /**
     * Stores data in a Dexie database.
     * @private
     * @param {string} url - The URL that was processed.
     * @param {Object} data - The extracted data.
     * @returns {Promise<void>}
     */
    async _storeInDexie(url, data) {
      if (!this.settings.db) {
        throw new Error('Dexie instance is not initialized.');
      }
      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}`);
      }
    }

    /**
     * Handles fetch errors with appropriate logging or actions.
     * @private
     * @param {Error} error - The error encountered.
     */
    _handleFetchError(error) {
      console.error('ScraperLib Fetch Error:', error);
      // Additional error handling actions can be added here.
    }

    /**
     * Implements fetch with retries.
     * @private
     * @param {string} url - The URL to fetch.
     * @param {Object} options - Fetch options.
     * @param {number} retries - Number of retries.
     * @returns {Promise<Response>} - The fetch response.
     */
    async _fetchWithRetries(url, options, retries) {
      for (let attempt = 0; attempt <= retries; attempt++) {
        try {
          const response = await fetch(url, options);
          if (!response.ok && attempt < retries) {
            await this._delay(this.settings.retryDelay);
            continue;
          }
          return response;
        } catch (error) {
          if (attempt < retries) {
            await this._delay(this.settings.retryDelay);
            continue;
          }
          throw error;
        }
      }
    }

    /**
     * Delays execution for a specified time.
     * @private
     * @param {number} ms - Milliseconds to delay.
     * @returns {Promise<void>}
     */
    _delay(ms) {
      return new Promise(resolve => setTimeout(resolve, ms));
    }

    /**
     * Cleans up active fetches to prevent memory leaks.
     * @private
     */
    _cleanUp() {
      // Placeholder for any cleanup operations if needed in the future.
      // Currently handled by removing fetch promises from activeFetches.
    }

    /**
     * Destroys the ScraperLib instance by aborting active fetches and cleaning up resources.
     */
    destroy() {
      // Abort all active fetches
      this.activeFetches.forEach(promise => {
        if (promise.abort) {
          promise.abort();
        }
      });
      this.activeFetches.clear();

      // Additional cleanup can be performed here
      this.settings = null;
    }

  }

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

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