Raw Source
sjehuda / Clean URL Improved

// ==UserScript==
// @name        Clean URL Improved
// @namespace   i2p.schimon.cleanurl
// @description Remove tracking parameters and redirect to original URL. This Userscript uses the URL Interface instead of RegEx.
// @homepageURL https://greasyfork.org/en/scripts/465933-clean-url-improved
// @supportURL  https://greasyfork.org/en/scripts/465933-clean-url-improved/feedback
// @updateURL   https://greasyfork.org/scripts/465933-clean-url-improved/code/Clean%20URL%20Improved.meta.js
// @downloadURL https://greasyfork.org/scripts/465933-clean-url-improved/code/Clean%20URL%20Improved.user.js
// @copyright   2023, Schimon Jehudah (http://schimon.i2p)
// @license     MIT; https://opensource.org/licenses/MIT
// @grant       none
// @run-at      document-end
// @match       file:///*
// @match       *://*/*
// @version     24.12.11
// @icon        data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDAgMTAwIj48dGV4dCB5PSIuOWVtIiBmb250LXNpemU9IjkwIj7wn5qlPC90ZXh0Pjwvc3ZnPgo=
// ==/UserScript==

// NOTE GM.getValue and GM.setValue
// will be used for statistics and
// custom lists

/*

NEWS

Handle /ref=

Added .replace(/\/ref=/g, '/?ref=')

Coding like a Japanese

TODO

1) Delete empty parameters

2) Statistics
    GM.getValue('links-total')
    GM.getValue('links-bad')
    GM.getValue('links-good')
    GM.getValue('parameters-total')
    GM.getValue('parameters-bad')
    GM.getValue('parameters-good')
    GM.getValue('parameters-urls')
    GM.getValue('parameters-unclassified')

    GM.setValue('pbl', 'hostname-parameter')
    GM.setValue('pwl', 'hostname-parameter')

3.1) Keep the position on the link (not near)

3.2) Focus cursor on the yellow or white spot.

3.3.) Be more strict on shopping websites (e.g. Remove parameter "tag").

FIXME

Whitelisted parameters without values are realized to purged-url

*/

/*

Simple version of this Userscript
let url = new URL(location.href);
if (url.hash || url.search) {
  location.href = url.origin + url.pathname
};

*/

// https://openuserjs.org/scripts/tfr/YouTube_Link_Cleaner

// Check whether HTML; otherwise, exit.
//if (!document.contentType == 'text/html')
if (document.doctype == null) return;

var itemColor, newURLItem, selectionItem, urlParameter;

//let point = [];
const namespace = 'i2p.schimon.cleanurl';

// List of url parameters
const urls = [
  'ap_id',
  'mortyurl',
  'redirect',
  'ref',
  'rf',
  'source',
  'src',
  'url',
  'utm_source',
  'utm_term'];

// List of alphabet
const alphabet = 'abcdefghijklmnopqrstuvwxyz';

// List of reserved parameters
const whitelist = [
  '_action',              // roundcube
  '_task',                // roundcube
  'act',                  // invision board
  'activeTab',            // npmjs
  'arch',                 // postmarketOS
  'architecture',         // distrowatch.com
  'art',                  // article
  'artist',               // bandcamp jamendo
  'action',               // bugzilla
  'author',               // git
  'ap_id',                // activitypub
  'bill',                 // law
  'basedon',              // distrowatch.com
  'board',                // simple machines forum
  'branch',               // postmarketOS
  'CategoryID',           // id
  'category',             // id
  'categories',           // searxng
  'catid',                // id
  'ci',                   // fossil-scm.org
  'code',                 // code
  'component',            // addons.palemoon.org
  'confirmation_token',   // dev.gajim.org
  'content',              // id
  'contributor',          // lulu
  'CurrentMDW',           // menuetos.be
  'dark',                 // yorik.uncreated.net
  'date',                 // date
  'days',                 // mediawiki
  'defaultinit',          // distrowatch.com
  'desktop',              // distrowatch.com
  'distribution',         // betterbird.eu
  'diff',                 // mediawiki
  'do',                   // flyspray
  'do_union',             // bugzilla
  'district',             // house.mo.gov
  'distrorange',          // distrowatch.com
  'engine',               // simplytranslate
  'exp_time',             // cdn
  'expires',              // cdn
  'ezimgfmt',             // cdn image processor
  'feed',                 // mediawiki
  'feedformat',           // mediawiki
  'fid',                  // mybb
  'file',                 // tw-cms
  'file_host',            // cdn
  'filename',             // filename
  'for',                  // cdn
  'format',               // file type
  'from',                 // redmine
  'func',                 // tw-cms
  'guid',                 // guid
  'group',                // bugzilla
  'group_id',             // sourceforge
  'hash',                 // cdn
  'hidebots',             // mediawiki
  'hit',                  // tasnimnews.com RSS
  'hl',                   // language
  'i2paddresshelper',     // i2p
  'id',                   // id
  'ie',                   // character encoding
  'ip',                   // ip address
  'isosize',              // distrowatch.com
  'item_class',           // greasyfork.org
  'item_id',              // greasyfork.org
  'iv_load_policy',       // invidious
  'jid',                  // jabber id (xmpp)
  'key',                  // cdn
  'lang',                 // language
  'language',             // searxng
  'library',              // openuserjs.org
  'limit',                // mediawiki
  'list',                 // video
  'locale',               // locale
  'logout',               // bugzilla
  'lr',                   // cdn
  'lra',                  // cdn
  'maintainer',           // postmarketOS
  'member',               // xmb forum
  'mobileaction',         // mediawiki
  'mode',                 // darkconquest.org
  'module',               // tw-cms
  'mortyurl',             // searxng
  'name',                 // archlinux
  'news_id',              // post
  'netinstall',           // distrowatch.com
  'node',                 // movim
  'notbasedon',           // distrowatch.com
  'nsm',                  // fossil-scm.org
  'oldid',                // mediawiki
  'op',                   // tw-cms
  'order',                // bugzilla
  'orderBy',              // openuserjs.org
  'orderDir',             // openuserjs.org
  'origin',               // openuserjs.org
  'ostype',               // distrowatch.com
  'outputType',           // xml
//'p',                    // search query / page number
  'package',              // distrowatch.com
  'page',                 // mybb
  'page_id',              // picapica.im
//'param',
  'pid',                  // fluxbb
  'playlistPosition',     // peertube
  'pkg',                  // distrowatch.com
  'pkgver',               // distrowatch.com
  'public_key',           // session
  'preferencesReturnUrl', // return url
  'product',              // bugzilla
  'project',              // flyspray
  'profile',              // copy.sh/v86/
  'pubsub',               // rivista xmpp journal
//'q',                    // search query
  'query',                // search query
  'query_format',         // bugzilla
  'redirect_to_referer',  // dev.gajim.org
  'redlink',              // mediawiki
  'ref_type',             // gitlab
//'referer',              // connect NOTE provided pathname contains connect or register
  'relation',             // distrowatch.com
  'repo',                 // postmarketOS
  'resolution',           // bugzilla
  'requestee',            // bugzilla
  'requester',            // bugzilla
  'return_to',            // connect
  'rolling',              // distrowatch.com
//'s',                    // search query
  'search',               // search query
  'selectedItem',         // atlassian
  'showtopic',            // invision board
  'show_all_versions',    // greasyfork.org
  'showforum',            // forums.duke4.net
//'si',                   // TODO make this specific to yt
  'sign',                 // cdn
  'signature',            // cdn
  'size',                 // rss
  'sort',                 // greasyfork.org
  'speed',                // cdn
  'ss',                   // fossil-scm.org
  'st',                   // invision board
  'start',                // page
  'start_time',           // media playback
  'status',               // distrowatch.com
  'subcat',               // solidtorrents
  'state',                // cdn
  'station',              // christiannetcast.com / truthradio.com
  '__switch_theme',       // theanarchistlibrary.org
//  'tag',                  // id
  'tags',                 // blasta
  'template',             // zapier
  'tid',                  // mybb
  'title',                // send (share) links and mediawiki
  'to',                   // gitlab
  'top',                  // hubzilla (atom syndication feed)
  'topic',                // simple machines forum
  'type',                 // file type
//'url',                  // url NOTE not sure whether to whitelist or blacklist
  'utf8',                 // encoding
  'urlversion',           // mediawiki
  'version',              // greasyfork.org
//'view',                 // invision board
  'view-source',          // git.sr.ht
  'vfx',                  // fossil-scm.org
//'_x_tr_sl', // translate online service
//'_x_tr_tl=', // translate online service
//'_x_tr_hl=', // translate online service
//'_x_tr_pto', // translate online service
//'_x_tr_hist', // translate online service
  'year'                  // year
  ];

// TODO 
// List of usefull hash
const goodHash = [
  'advanced',             // distrowatch.com
  'simple',               // distrowatch.com
  ];

// List of useless hash
const hash = [
  'back-url',
  'back_url',
  'intcid',
  'niche-',
//'searchinput',
  'src'];

// List of useless parameters
const blacklist = [
  'ad',
  'ad-location',
  'ad_medium',
  'ad_name',
  'ad_pvid',
  'ad_sub',
//'ad_tags',
  'advertising-id',
//'aem_p4p_detail',
  'af',
  'aff',
  'aff_fcid',
  'aff_fsk',
  'aff_platform',
  'aff_trace_key',
  'affiliate',
  'affparams',
  'afftrack',
  'afSmartRedirect',
//'aid',
  'algo_exp_id',
  'algo_pvid',
  'ar',
//'ascsubtag',
//'asc_contentid',
  'asgtbndr',
  'atc',
  'ats',
  'autostart',
//'b64e', // breaks yandex
  'bizType',
//'block',
  '_bsa_req',
  'bta',
  'businessType',
  'campaign',
  'campaignId',
//'__cf_chl_rt_tk',
//'cid', // breaks sacred magick
  'ck',
//'clickid',
//'client_id',
//'cm_ven',
//'cmd',
  'content-id',
  'crid',
  'cst',
  'cts',
  'curPageLogUid',
//'data', // breaks yandex
//'dchild',
//'dclid',
  'deals-widget',
  'dgcid',
  'dicbo',
//'dt',
  'edd',
  'edm_click_module',
//'ei',
//'embed',
//'_encoding',
//'etext', // breaks yandex
  'eventSource',
  'fbclid',
  'feature',
  'field-lbr_brands_browse-bin',
  'forced_click',
//'fr',
  'frs',
//'from', // breaks yandex
  '_ga',
  'ga_order',
  'ga_search_query',
  'ga_search_type',
  'ga_view_type',
  'gatewayAdapt',
//'gclid',
//'gclsrc',
  'gh_jid',
  'gps-id',
//'gs_lcp',
  'gt',
  'guccounter',
  'hdtime',
//'hosted_button_id',
  'ICID',
  'ico',
  'ig_rid',
//'idzone',
//'iflsig',
  'intake',
  'intcmp',
  'irclickid',
//'irgwc',
//'irpid',
  'is_from_webapp',
  'itid',
//'itok',
//'katds_labels',
//'keywords',
  'keyno',
  'l10n',
  'linkCode',
  'mc',
  'mid',
  '__mk_de_DE',
  'mp',
  'nats',
  'nci',
  'obOrigUrl',
  'offer_id',
  'opened-from',
  'optout',
  'oq',
  'organic_search_click',
  'pa',
  'Partner',
  'partner',
  'partner_id',
  'partner_ID',
  'pcampaignid',
  'pd_rd_i',
  'pd_rd_r',
  'pd_rd_w',
  'pd_rd_wg',
  'pdp_npi',
  'pf_rd_i',
  'pf_rd_m',
  'pf_rd_p',
  'pf_rd_r',
  'pf_rd_s',
  'pf_rd_t',
  'pg',
  'PHPSESSID',
  'pk_campaign',
  'pdp_ext_f',
  'pkey',
  'platform',
  'plkey',
  'pqr',
  'pr',
  'pro',
  'prod',
  'prom',
  'promo',
  'promocode',
  'promoid',
  'psc',
  'psprogram',
  'pvid',
  'qid',
//'r',
  'realDomain',
  'recruiter_id',
  'redirect',
  'ref',
  'ref_',
  'ref_src',
  'refcode',
  'referral',
  'referrer',
  'refinements',
  'reftag',
  'rf',
  'rnid',
  'rowan_id1',
  'rowan_msg_id',
//'rss',
//'sCh',
  'sclient',
  'scm',
  'scm_id',
  'scm-url',
//'sd',
  'sender_device',
  'sh',
  'shareId',
  'showVariations',
//'si',
//'sid', // breaks whatsup.org.il
  '___SID',
//'site_id',
  'sk',
  'smid',
  'social_params',
  'source',
  'sourceId',
  'sp_csd',
  'spLa',
  'spm',
  'spreadType',
//'sprefix',
  'sr',
  'src',
  '_src',
  'src_cmp',
  'src_player',
  'src_src',
  'srcSns',
  'su',
//'sxin_0_pb',
  '_t',
//'tag',
  'tcampaign',
  'td',
  'terminal_id',
//'text',
  'th', // Sometimes restored after page load
//'title',
  'tracelog',
  'traffic_id',
  'traffic_source',
  'traffic_type',
  'tt',
  'uact',
  'ug_edm_item_id',
  'utm',
//'utm1',
//'utm2',
//'utm3',
//'utm4',
//'utm5',
//'utm6',
//'utm7',
//'utm8',
//'utm9',
  'utm_campaign',
  'utm_content',
  'utm_id',
  'utm_medium',
  'utm_source',
  'utm_term',
  'uuid',
//'utype',
//'ve',
//'ved',
//'zone'
  ];

// URL Indexers
const paraIDX = [
  'algo_exp_id',
  'algo_pvid',
  'b64e',
  'cst',
  'cts',
  'data',
  'ei',
//'etext',
  'from',
  'iflsig',
  'gbv',
  'gs_lcp',
  'hdtime',
  'keyno',
  'l10n',
  'mc',
  'oq',
//'q',
  'sei',
  'sclient',
  'sign',
  'source',
  'state',
//'text',
  'uact',
  'uuid',
  'ved'];

// Market Places 
const paraMKT = [
  '___SID',
  '_t',
  'ad_pvid',
  'af',
  'aff_fsk',
  'aff_platform',
  'aff_trace_key',
  'afSmartRedirect',
  'bizType',
  'businessType',
  'ck',
  'content-id',
  'crid',
  'curPageLogUid',
  'deals-widget',
  'edm_click_module',
  'gatewayAdapt',
  'gps-id',
  'keywords',
  '__mk_de_DE',
  'pd_rd_i',
  'pd_rd_r',
  'pd_rd_w',
  'pd_rd_wg',
  'pdp_npi',
  'pf_rd_i',
  'pf_rd_m',
  'pf_rd_p',
  'pf_rd_r',
  'pf_rd_s',
  'pf_rd_t',
  'platform',
  'pdp_ext_f',
  'ref_',
  'refinements',
  'rnid',
  'rowan_id1',
  'rowan_msg_id',
  'scm',
  'scm_id',
  'scm-url',
  'shareId',
//'showVariations',
  'sk',
  'smid',
  'social_params',
  'spLa',
  'spm',
  'spreadType',
  'sprefix',
  'sr',
  'srcSns',
//'sxin_0_pb',
  'terminal_id',
  'th', // Sometimes restored after page load
  'tracelog',
  'tt',
  'ug_edm_item_id'];

// IL
const paraIL = [
  'dicbo',
  'obOrigUrl'];

// General
const paraWWW = [
  'aff',
  'promo',
  'promoid',
  'ref',
  'utm_campaign',
  'utm_content',
  'utm_medium',
  'utm_source',
  'utm_term'];

// For URL of the Address bar
// Check and modify page address
// TODO Add bar and ask to clean address bar
(function modifyURL() {

  let
    check = [],
    // NOTE Marketplace website which uses /ref= instead of ?ref=
    // location.href.replace(/\/ref=/g, '/?ref=');
    url = new URL(location.href.replace('/ref=', '?ref='));

  // TODO turn into boolean function
  for (let i = 0; i < blacklist.length; i++) {
    if (url.searchParams.get(blacklist[i])) {
      check.push(blacklist[i]);
      url.searchParams.delete(blacklist[i]);
      //newURL = url.origin + url.pathname + url.search + url.hash;
    }
  }

  // TODO turn into boolean function
  for (let i = 0; i < hash.length; i++) {
    if (url.hash.startsWith('#' + hash[i])) {
      check.push(hash[i]);
      //newURL = url.origin + url.pathname + url.search;
    }
  }

  if (check.length > 0) {
    let newURL = url.origin + url.pathname + url.search;
    window.history.pushState(null, null, newURL);
    //location.href = newURL;
  }

})();

// NOTE Marketplace website which uses /ref= instead of ?ref=
(function correctSlashRefURLs() {
  for (let i = 0; i < document.links.length; i++) {
    if (document.links[i].href.includes('/ref=')) {
      document.links[i].href = document.links[i].href.replace('/ref=', '?ref=');
      document.links[i].setAttribute('slash-ref', '');
    }
  }
})();

(function scanAllURLs() {
  for (let i = 0; i < document.links.length; i++) {
    let url = new URL(document.links[i].href);
    // NOTE Consider BitTorrent Magnet links
    // removing trackers would need a warning about
    // private torrents, if torrent is not public (dht-enabled)
    const allowedProtocols = [
      'finger:', 'freenet:', 'gemini:',
      'gopher:', 'wap:', 'ipfs:',
      'https:', 'ftps:', 'http:', 'ftp:'];
    if (url.search && allowedProtocols.includes(url.protocol)) {
    //if (url.search || url.hash) {
      document.links[i].setAttribute('href-data', document.links[i].href);
    }
  }
})();

(function scanBadURLs() {
  for (let i = 0; i < document.links.length; i++) {
    // TODO callback, Mutation Observer, and Event Listener
    // TODO Count links increaseByOne('links')
    // NOTE To count links, add return statement to function cleanLink()
    // return statement will indicate that link is positive for subject
    // parameters and therefore should be counted. Counter will be added
    // by one, once detected that url is not equal to (new) url.
    hash.forEach(j => cleanLink(document.links[i], j, 'hash'));
    blacklist.forEach(j => cleanLink(document.links[i], j, 'para'));
  }
})();

// TODO Add an Event Listener
function cleanLink(link, target, type) {
  let url = new URL(link.href);
  switch (type) {
    case 'hash':
      //console.log('hash ' + i)
      if (url.hash.startsWith('#' + target)) {
        //link.setAttribute('href-data', link.href);
        link.href = url.origin + url.pathname + url.search;
        //increaseByOne('hashes')
      }
      break;
    case 'para':
      //console.log('para ' + i)
      if (url.searchParams.get(target)) { 
        url.searchParams.delete(target);
        //link.setAttribute('href-data', link.href);
        link.href = url.origin + url.pathname + url.search;
        //increaseByOne('parameters')
      }
      break;
  }

  /*
  // EXTRA
  // For URL of hyperlinks
  for (const a of document.querySelectorAll('a')) {
    try{
      let url = new URL(a.href);
      for (let i = 0; i < blacklist.length; i++) {
        if (url.searchParams.get(blacklist[i])) {
          url.searchParams.delete(blacklist[i]);
        }
      }
      a.href = url;
    } catch (err) {
      //console.warn('Found no href for element: ' + a);
      //console.error(err);
    }
  } */

}

// TODO Hunt (for any) links within attributes using getAttributeNames()[i]

// Event Listener
// TODO Scan 'e.target.childNodes' until 'href-data' (link) is found
document.body.addEventListener("mouseover", function(e) { // mouseover works with keyboard too
  //if (e.target && e.target.nodeName == "A") {
  let hrefData = e.target.getAttribute('href-data');
  //if (e.target && hrefData && !document.getElementById(namespace)) {
  if (e.target && hrefData && hrefData != document.getElementById('url-original')) {
    if (document.getElementById(namespace)) {
      document.getElementById(namespace).remove();
    }
    selectionItem = createButton(e.pageX, e.pageY, hrefData);
    hrefData = new URL(hrefData);
    selectionItem.append(purgeURL(hrefData));
    let types = ['whitelist', 'blacklist', 'original'];
    for (let i = 0; i < types.length; i++) {
      let button = purgeURL(hrefData, types[i]);
      if (types[i] == 'original' && e.target.getAttribute('slash-ref') == '') {
        button.href = button.href.replace('?ref=', '/ref=');
      }
      let exist;
      selectionItem.childNodes.forEach(
        node => {
          if (button.href == node.href) {
            exist = true;
          }
        }
      );
      if (!exist) {
        selectionItem.append(button);
      }
    }

    // Check for URLs
    for (let i = 0; i < urls.length; i++) {
      if (hrefData.searchParams.get(urls[i])) { // hrefData.includes('url=')
        urlParameter = hrefData.searchParams.get(urls[i]);
        try {
          urlParameter = new URL (urlParameter);
        } catch {
          if (urlParameter.includes('.')) {  // NOTE It is a guess
            try {
              urlParameter = new URL ('http:' + urlParameter);
            } catch {}
          }
        }
        if (typeof urlParameter == 'object' && // confirm url object
            urlParameter != location.href) {   // provided url isn't the same as of page
          newURLItem = extractURL(urlParameter);
          selectionItem.prepend(newURLItem);
        }
      }
    }

    /*
    // compare original against purged
    //if (selectionItem.querySelector(`#url-purged`) &&
    //    selectionItem.querySelector(`#url-original`)) {
    if (selectionItem.querySelector(`#url-purged`)) {
      //let urlOrigin = new URL (selectionItem.querySelector(`#url-original`).href);
      let urlPurge = new URL (selectionItem.querySelector(`#url-purged`).href);
      // NOTE
      // These "searchParams.sort" ~~may be~~ *are not* redundant.
      // See resUrl.searchParams.sort()
      urlPurge.searchParams.sort();
      hrefData.searchParams.sort();
      //console.log(hrefData.search);
      //console.log(urlPurge.search);
      if (hrefData.search == urlPurge.search &&
          selectionItem.querySelector(`#url-original`)) {
        selectionItem.querySelector(`#url-original`).remove();
      }
    } else
    // compare original against safe
    if (selectionItem.querySelector(`#url-known`)) {
      //let urlOrigin = new URL (selectionItem.querySelector(`#url-original`).href);
      let urlKnown = new URL (selectionItem.querySelector(`#url-known`).href);
      // NOTE
      // These "searchParams.sort" ~~may be~~ *are not* redundant.
      // See resUrl.searchParams.sort()
      urlKnown.searchParams.sort();
      hrefData.searchParams.sort();
      //console.log(hrefData.search);
      //console.log(urlKnown.search);
      if (hrefData.search == urlKnown.search &&
          selectionItem.querySelector(`#url-original`)) {
        selectionItem.querySelector(`#url-original`).remove();
      }
    }
    */

    // compare original against safe and purged
    // NOTE on "item.href = decodeURI(resUrl)"
    // The solution was here.
    // Decode was not the issue
    // This is a good example to show that
    // smaller tasks are as important as bigger tasks
    let urlsToCompare = ['#url-known', '#url-purged'];
    for (let i = 0; i < urlsToCompare.length; i++) {
      if (selectionItem.querySelector(urlsToCompare[i])) {
        //let urlOrigin = new URL (selectionItem.querySelector(`#url-original`).href);
        let urlToCompare = new URL (selectionItem.querySelector(urlsToCompare[i]).href);
        // NOTE
        // These "searchParams.sort" ~~may be~~ *are not* redundant.
        // See resUrl.searchParams.sort()
        urlToCompare.searchParams.sort();
        hrefData.searchParams.sort();
        //console.log(hrefData.search);
        //console.log(urlToCompare.search);
        if (hrefData.search == urlToCompare.search &&
            selectionItem.querySelector(`#url-original`)) {
          selectionItem.querySelector(`#url-original`).remove();
        }
      }
    }

    // do not add element, if url has only whitelisted parameters and no potential url
    // add element, only if a potential url or non-whitelisted parameter was found
    let urlTypes = ['url-extracted', 'url-original', 'url-purged'];
    for (let i = 0; i < urlTypes.length; i++) {
      if (selectionItem.querySelector(`#${urlTypes[i]}`)) {
        document.body.append(selectionItem);
        return;
      }
    }

    // NOTE in case return did not reach
    // it means that there is no link to process
    e.target.removeAttribute('href-data');

    //if (!e.target.getAttribute('slash-ref') == '') {
    //  e.target.removeAttribute('href-data')
    //}

  }
});

function createButton(x, y, url) {
  // create element
  let item = document.createElement(namespace);
  // set content
  item.id = namespace;
  // set position
  item.style.all = 'unset';
  item.style.position = 'absolute';
  //item.style.left = x+5 + 'px';
  //item.style.top = y-3 + 'px';
  item.style.left = x+45 + 'px';
  item.style.top = y-65 + 'px';
  // set appearance
  item.style.fontFamily = 'none'; // emoji
  item.style.background = '#333';
  item.style.borderRadius = '5%';
  item.style.padding = '3px';
  item.style.direction = 'ltr';
  item.style.zIndex = 10000;
  //item.style.opacity = 0.7;
  //item.style.filter = 'brightness(0.7) drop-shadow(2px 4px 6px black)';
  item.style.filter = 'brightness(0.7)';
  // center character
  item.style.justifyContent = 'center';
  item.style.alignItems = 'center';
  item.style.display = 'flex';
  // disable selection marks
  item.style.userSelect = 'none';
  item.style.cursor = 'default';
  // set button behaviour
  item.onmouseover = () => {
    //item.style.opacity = 1;
    //item.style.filter = 'drop-shadow(2px 4px 6px black)';
    item.style.filter = 'unset';
  };
  item.onmouseleave = () => { // onmouseout
    // TODO Wait a few seconds
    item.remove();
  };
  return item;
}

function extractURL(url) {
  let item = document.createElement('a');
  item.textContent = '🔗'; // 🧧 🏷️ 🔖
  item.title = 'Extracted URL';
  item.id = 'url-extracted';
  item.style.all = 'unset';
  item.style.outline = 'none';
  item.style.height = '15px';
  item.style.width = '15px';
  item.style.padding = '3px';
  item.style.margin = '3px';
  //item.style.fontSize = '0.9rem' // 90%
  item.style.lineHeight = 'normal'; // initial
  //item.style.height = 'fit-content';
  item.href = url;
  return item;
}

// TODO Use icons (with shapes) for cases when color is not optimal
function purgeURL(url, listType) {
  let orgUrl = null;
  let itemTitle, itemId, resUrl;
  let item = document.createElement('a');
  item.style.all = 'unset';
  switch (listType) {
    case 'blacklist':
      itemColor = 'yellow';
      //itemTextContent = '🟡';
      itemTitle = 'Semi-safe link'; // Purged URL
      itemId = 'url-purged';
      resUrl = hrefDataHandler(url, blacklist);
      break;
    case 'original': // TODO dbclick (double-click)
      itemColor = 'orangered';
      //itemTextContent = '🔴';
      itemTitle = 'Unsafe link'; // Original URL
      itemId = 'url-original';
      //resUrl = encodeURI(url);
      // NOTE By executing url.searchParams.sort()
      // we change the order of parameters
      // which means that we create a new and unique url
      // which means that it can be used to identify users that use this program
      // NOTE We execute url.searchParams.sort()
      // in order to avoid false positive for url of blacklisted parameters
      // but we don't apply that change on item "url-original"
      //url.searchParams.sort();
      orgUrl = url;
      item.style.cursor = `not-allowed`; // no-drop
      item.onmouseenter = () => {
        item.style.background = 'darkorange';
        item.style.filter = `drop-shadow(2px 4px 6px ${itemColor})`;
      };
      item.onmouseout = () => {
        item.style.background = itemColor;
        item.style.filter = 'unset';
      };
      break;
    case 'whitelist':
      itemColor = 'lawngreen';
      //itemTextContent = '🟢';
      itemTitle = 'Safe link'; // Link with whitelisted parameters
      itemId = 'url-known';
      resUrl = hrefDataHandler(url, whitelist);
      break;
    default:
      itemColor = 'antiquewhite';
      //itemTextContent = '⚪';
      itemTitle = 'Base link'; // Link without parameters
      itemId = 'url-base';
      resUrl = url.origin + url.pathname;
      resUrl = new URL(resUrl); // NOTE To avoid error in resUrl.searchParams.sort()
      break;
  }
  item.id = itemId;
  item.title = itemTitle;
  item.style.background = itemColor;
  //item.textContent = itemTextContent;
  item.style.borderRadius = '50%';
  item.style.outline = 'none';
  item.style.height = '15px';
  item.style.width = '15px';
  item.style.padding = '3px';
  item.style.margin = '3px';
  if (orgUrl){
    item.href = orgUrl;
  } else {
    // NOTE Avoid duplicates by sorting parameters of all links
    resUrl.searchParams.sort();
    // NOTE Avoid false positive by decoding
    // TODO decode from ?C=N%3BO%3DD to ?C=N;O=D
    // FIXME decodeURI doesn't appear to work
    // Text page https://mirror.lyrahosting.com/gnu/a2ps/ (columns raw)
    // SOLVED See "urlsToCompare"
    //item.href = decodeURI(resUrl);
    //item.href = decodeURIComponent(resUrl);
    // TODO whitespace turns into plus and
    // then red/white appears for the same uri
    item.href = resUrl;
  }
  return item;
}

// NOTE The URL API doesn't list parameters
// without explicitly calling them, therefore
// reading lengths of unknown parameters is
// impossible, hence
// set a loop from Aa - Zz or
// add Aa - Zz to whitelist
function hrefDataHandler(url, listType) {
  url = new URL(url.href);
  // NOTE Avoid duplicates by sorting parameters of all links
  //url.searchParams.sort();
  switch (listType) {
    case whitelist:
      let newURL = new URL (url.origin + url.pathname);
      for (let i = 0; i < whitelist.length; i++) {
        if (url.searchParams.get(whitelist[i])) {
          newURL.searchParams.set(
            whitelist[i],
            url.searchParams.get(whitelist[i]) // catchedValue
          );
        }
      }

      // Whitelist parameters of single character long
      let a2z = alphabet.split('');
      for (let i = 0; i < a2z.length; i++) {
        if (url.searchParams.get(a2z[i])) {
          newURL.searchParams.set(
            a2z[i],
            url.searchParams.get(a2z[i])
          );
        }
      }

      let A2Z = alphabet.toUpperCase().split('');
      for (let i = 0; i < A2Z.length; i++) {
        if (url.searchParams.get(A2Z[i])) {
          newURL.searchParams.set(
            A2Z[i],
            url.searchParams.get(A2Z[i])
          );
        }
      }

      /*
      let a2z = genCharArray('a', 'z');
      for (let i = 0; i < a2z.length; i++) {
        if (url.searchParams.get(a2z[i])) {
          newURL.searchParams.set(
            a2z[i],
            url.searchParams.get(a2z[i]) // catchedValue
          );
        }
      }

      let A2Z = genCharArray('A', 'Z');
      for (let i = 0; i < A2Z.length; i++) {
        if (url.searchParams.get(A2Z[i])) {
          newURL.searchParams.set(
            A2Z[i],
            url.searchParams.get(A2Z[i]) // catchedValue
          );
        }
      }
      */

      url = newURL;
      break;
    case blacklist:
      for (let i = 0; i < blacklist.length; i++) {
        if (url.searchParams.get(blacklist[i])) {
          url.searchParams.delete(blacklist[i]);
          //increaseByOne('parameters')
        }
      }
      //increaseByOne('links')
      break;
  }
  return url;
}

// /questions/24597634/how-to-generate-an-array-of-the-alphabet
function genCharArray(charA, charZ) {
  var a = [], i = charA.charCodeAt(0), j = charZ.charCodeAt(0);
  for (; i <= j; ++i) {
    a.push(String.fromCharCode(i));
  }
  return a;
}

async function increaseByOne(key) {
  let currentValue = await GM.getValue(key, 0);
  await GM.setValue(key, currentValue + 1);
  console.log(key);
  console.log(currentValue);
}

// NOTE Marketplace website which uses /ref= instead of ?ref=
// Check for .getAttribute('slash-ref') == ''
function deleteSerialNumber(url) {
  let newURL = [];
  let pattern = /^[0-9\-]+$/;
  let oldUrl = url.toString().split('/');
  for (const cell of oldUrl) {
    if (!pattern.test(cell)) {
      newURL.push(cell);
    }
  }
  newURL = newURL.join('/');
  return new URL(newURL);
}