- // ==UserScript==
- // @name Proxified Links DEBUG
- // @author proxi
- // @homepageURL https://greasyfork.org/en/scripts/485274-proxified-links
- // @copyright 2023 Schimon Jehudah (http://schimon.i2p)
- // @license AGPL-3.0-only; https://www.gnu.org/licenses/agpl-3.0.en.html
- // @namespace com.proxi.proxified
- // @description Proxified hyperlinks to a proxy instance or Farside with no nonsense
- // Add or remove preferred services and instances yourself!
- //
- // Forked from Proxify Links v23.10.17, by Schimon Jehudah, and modified with prejudice and style
- // - Proxy, proxy, proxy; see and configure non-proxying frontends yourself before opening links
- // - Use a small hardcoded list of favorite proxy instances, or:
- // - Hold X key to use Farside redirection where possible
- // - Hold Z key to use the original link whenever needed
- // - Easily use first-party frontends with discretion or when instances are down
- // - Clearnet by default following Farside, add personal lists of decentralized nodes
- // - Set noreferrer on supported links, optionally all links if `ENABLE_REFERER_HIDE_PAGEWIDE`,
- // preventing referer header while browsing without breaking common ajax functionality
- // - Use GET requests on supported search engines particularly with noscript
- // (Recommend disabled if `ENABLE_REFERER_HIDE` is off)
- // @run-at document-end
- // @version 0.3.0
- // @match *://*/*
- // @icon data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgOTAwMCA5MDAwIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbDpzcGFjZT0icHJlc2VydmUiPjxyZWN0IHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InllbGxvdyIvPjxwYXRoIGQ9Ik0uMjc5LS4xNDYuMTUgMGgtLjE3MmwuMjQtLjI2Ni0uMTMyLS4yNTNoLjE0OWwuMDc2LjE0OC4xMzEtLjE0OGguMTdsLS4yNC4yNjkuMTMuMjVILjM1NEwuMjc5LS4xNDZaIiBzdHlsZT0iZmlsbDp3aGl0ZTsiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDE2NzUgNzcyOCkgc2NhbGUoODAwMCkiLz48cGF0aCBkPSJNLjE4OCAwSC4wNDFMLjE5LS43MTZoLjI5MWEuMzQuMzQgMCAwIDEgLjEyMy4wMThjLjAzLjAxMy4wNTQuMDMzLjA3Mi4wNjFhLjE5Mi4xOTIgMCAwIDEgLjAyNi4xMDEuMjczLjI3MyAwIDAgMS0uMDIxLjEwNS4yNC4yNCAwIDAgMS0uMDUxLjA4Mi4yMzcuMjM3IDAgMCAxLS4xNjEuMDcxIDEuMTY2IDEuMTY2IDAgMCAxLS4xMy4wMDVILjI0NUwuMTg4IDBaTS4yNy0uMzkyaC4wNDVhLjUzLjUzIDAgMCAwIC4xNTYtLjAxNC4xMzEuMTMxIDAgMCAwIC4wNjEtLjA0Ny4xMjMuMTIzIDAgMCAwIC4wMjMtLjA3MS4wNy4wNyAwIDAgMC0uMDEyLS4wNDIuMDYyLjA2MiAwIDAgMC0uMDMyLS4wMjUuMzU4LjM1OCAwIDAgMC0uMDkzLS4wMDdILjMxM0wuMjctLjM5MloiIHRyYW5zZm9ybT0ibWF0cml4KDkzMjAgMCAwIDkzMTMgMTEyOCA3NzI4KSIvPjxwYXRoIGQ9Im0yNTEuNjg0IDY1NS45ODkuMTYyLjIzIDUyMi40ODYgMTczLjI5Mi0xMjEuMjk4LTE3My4zNTItLjUwMy0uMTdIMjUxLjY4NFptMTM0OS40NzYgMzE4LjYxLTI4My45OS05NS4wMTZjLTY0LjE5LS45NjItMTQ4LjMyLTEuNDQzLTI1Mi4zOS0xLjQ0M0g4MDguMzU5bC0xNy43MjMtMjUuMzI4LTIxMi4wNTQgMTIxLjc4N2gzNDYuMzg0bDEwMi4wODQtNTkuNzM3IDE3Ny4xNiA1OS43MzdoMzk2Ljk1WiIgc3R5bGU9ImZpbGw6eWVsbG93Z3JlZW4iIHRyYW5zZm9ybT0ibWF0cml4KDMuNDMgMCAwIC0xMS40NCA2NDIgMTUyMzUuNikiLz48cGF0aCBkPSJNMTUwMCA4MTIwaDU4MDB2MzYwSDE3NTB6Ii8+IDwvc3ZnPg==
- // ==/UserScript==
-
- /**
- * Basic configurations for functionality and performance
- */
- const KEY_MODIFIED = 'x'; // X key to modify clicked link to Farside, if applicable
- const KEY_BONAFIDE = 'z'; // Z key to revert clicked link to the original destination
- const TOUCH_MODIFIED = 3; // Simple 3-finger tap gesture to modify tapped link to Farside, if applicable
- const TOUCH_BONAFIDE = 5; // Simple 5-finger tap gesture to revert tapped link to the original destination
- const TOUCH_PROXIFIED = 2; // Reserved 2-finger tap gesture to reset tapped link to static proxified
- const ROTATE_SITE_PROXIFIED = true; // Optional: Rotate proxy instances instead of random to prevent adjacent duplicates
- const ENABLE_RERENDER = true; // Optional: Forcibly rerender for browser to update UI, may degrade performance
-
- // Optional keys to limit unintentional triggering on non-links
- // Set to empty '' string, proxification will happen automatically on selection
- // TODO: improve touch support
- const KEY_PROXIBITE = 'b'; // Optional B key to allow heavier elements to be proxified
- const KEY_FRAMEBITE = 'f'; // Optional B+F key combination to trigger when iframes are updated
- const TOUCH_PROXIBITE = 4; // Simple 4-finger tap gesture to allow all heavier elements to be proxified
- const TOUCH_FRAMEBITE = 4;
- // TODO: support multiple keys to whitelist specific keys that will trigger handlers, consider screenreader controls
- const KEY_NAVIGATE = ''; // Optional filter for key nav used to proxify the selected element, e.g Tab
-
- // Optional proxified iframes with supported src, proxified immediately when selected
- // Best used if iframe content is already blocked by a content blocker (e.g. ublock click2load),
- // so the original frame is never loaded automatically even if switching back from proxified (B+F+Z)
- const ENABLE_IFRAME_PROXIFIED = true;
-
- // Optional noreferer override on all proxified links
- // If disabled, only upgrade undefined or noopener to noreferrer for proxified links
- // Disable if proxified links work but bona fide (Z) links are breaking
- const ENABLE_REFERER_HIDE = true;
-
- // Optional noreferer override on all links on the page, if ENABLE_REFERER_HIDE is also on
- // Disable if unrelated links or site authentication windows are breaking
- const ENABLE_REFERER_HIDE_PAGEWIDE = true;
-
- // Optional proxified links using query string if the main url does not match a proxy
- // Useful for tracking links or old-fashioned HTML GET search, e.g. DuckDuckGo (uddg)
- // Strongly recommend used with `ENABLE_REFERER_HIDE_PAGEWIDE` to avoid exposing search engine and keywords
- // Disable if random links that shouldn't be proxified still are
- const ENABLE_QUERY_PROXIFIED = true;
- const ENABLE_QUERY_PROXIFIED_ON = []; // if empty, all query parameters will be included
- const ENABLE_QUERY_PROXIFIED_OFF = ['piurl' /* Startpage image proxy */]; // blacklisted query parameters
-
- // Optional stripping links of attributes that are necessary to proxify links (Google),
- // and/or optional removal of link trackers on sites or search engines
- const ENABLE_ATTRIBUTES_SMITE = true;
- /** @type {{ [host: string]: {attributes: string[], allowFuzzy?: boolean} }} */
- const ENABLE_ATTRIBUTES_SMITE_ON_SITE = { 'google.com': { attributes: ['data-sb'] } };
-
- // Optional search engine GET requests instead of POST for improved search navigation
- // Used with ENABLE_REFERER_HIDE and ENABLE_REFERER_HIDE_PAGEWIDE on
- // CAUTION: May be unsupported, and subject to negligent server logging query string
- const ENABLE_SEARCH_GET = true;
- /** @type {{ [host: string]: { formSelector: string } }} */
- const ENABLE_SEARCH_GET_ON_SITE = {
- 'html.duckduckgo.com': { formSelector: 'form:has(#search_form_input_homepage), form:has(.btn[type="submit"])' },
- 'startpage.com': { formSelector: '#search, form:has(.header-nav-item), form:has(button[type="submit"]' },
- };
-
- /**
- * Configure site settings or add an instance of a proxy to the site's `redirect` list
- *
- * As is, use a shortlist of useful instances and mature proxy services
- * Out of box is an opinionated, not comprehensive or up-to-date, list of useful instances,
- * not ordered alphabetically but in the order of best proxied or most commonly linked
- *
- * @typedef {{
- * redirect?: {
- * replacements?: string[],
- * suffix?: string, // optional suffix to a random replacement host from `replacements`
- * routes?: ProxyRoute[], // optional route whitelist by matching url regex, defaults to replacing url host only if undefined
- * inherit?: string // inherit any undefined properties in `redirect`
- * },
- * redirectToFarside?: {
- * replacements?: string[],
- * suffix?: string, // optional suffix to a random replacement host from `replacements`
- * routes?: ProxyRoute[], // optional route whitelist by matching url regex, defaults to replacing url host only if undefined
- * inherit?: string // inherit any undefined properties in `redirectToFarside`
- * },
- * allowFuzzy?: boolean, // allows fuzzy host matching, for subdomains
- * allowIframe?: boolean, // allows matching on iframe src
- * inherit?: string // inherit any undefined redirect rules
- * }} Proxy
- * @typedef {{
- * regex: RegExp, // route regex match
- * suffix?: string, // optional route suffix on host, appending to parent `suffix` if defined
- * }} ProxyRoute
- * @typedef {{ [host: string]: Proxy }} ProxyList
- */
-
- /**
- * @type {ProxyList}
- */
- const PROXIES = {
- // youtube.com, youtu.be, m.youtube.com, youtube-nocookie.com
- // /watch, /trending, /@, /channel/
- // Allows iframe matching too, which is useful when content blocking embeds
- // e.g. ublock ||youtube.com^$3p,frame,redirect=click2load.html
- 'youtube.com': {
- allowIframe: true,
- redirect: {
- replacements: [
- 'https://piped.video',
- 'https://piped.smnz.de',
- //'https://piped.projectsegfau.lt', // down
- 'https://piped.privacydev.net',
- //'https://piped.lunar.icu', // embedded frame blocked by x-frame-options
- 'https://piped.adminforge.de',
- //'https://pd.vern.cc', // low availability
- // The following Invidious instances not only allow video proxy but proxy by default
- //'https://iv.datura.network', // proxy off by default
- //'https://invidious.projectsegfau.lt', // down
- //'https://invidious.fdn.fr', // down
- ],
- },
- redirectToFarside: {
- // Only use Farside's Piped redirect since most Invidious instances do not proxy videos by default
- // Aside from proxy by default, Invidious is preferred for nojs, configuration, and download functionality
- // Specific Invidious instances that proxy by default are included in the instance `redirect` list
- replacements: ['https://farside.link/piped'],
- },
- },
- 'youtu.be': {
- redirect: {
- inherit: 'youtube.com',
- routes: [
- {
- regex: /^https?:\/\/(www\.)?youtu\.be\/([A-Za-z0-9_-]+)\??(.*)$/,
- suffix: '/watch?v=$2&$3', // manually add in path and params to support invidious https://github.com/iv-org/invidious/issues/3933
- },
- ],
- },
- redirectToFarside: {
- inherit: 'youtube.com',
- routes: [
- {
- regex: /^https?:\/\/(www\.)?youtu\.be\/([A-Za-z0-9_-]+)\??(.*)$/,
- suffix: '/watch?v=$2&$3', // manually add in path and params to support invidious https://github.com/iv-org/invidious/issues/3933
- },
- ],
- },
- },
- 'm.youtube.com': { inherit: 'youtube.com' },
- 'youtube-nocookie.com': { inherit: 'youtube.com' },
-
- // reddit.com
- 'reddit.com': {
- redirect: {
- replacements: [
- //'https://libreddit.projectsegfau.lt', // low availability
- //'https://libreddit.privacydev.net', // low availability
- 'https://l.opnxng.com',
- //'https://reddit.invak.id', // down
- 'https://libreddit.kavin.rocks',
- 'https://red.artemislena.eu',
- ],
- },
- redirectToFarside: {
- // teddit no longer actively maintained: https://codeberg.org/teddit/teddit
- replacements: ['https://farside.link/libreddit'],
- },
- },
- // redd.it image shortlinks
- 'i.redd.it': {
- redirect: {
- inherit: 'reddit.com',
- suffix: '/img',
- },
- redirectToFarside: {
- inherit: 'reddit.com',
- suffix: '/img',
- },
- },
- 'preview.redd.it': {
- redirect: {
- inherit: 'reddit.com',
- suffix: '/preview/pre',
- },
- redirectToFarside: {
- inherit: 'reddit.com',
- suffix: '/preview/pre',
- },
- },
- 'external-preview.redd.it': {
- redirect: {
- inherit: 'reddit.com',
- suffix: '/preview/external-pre',
- },
- redirectToFarside: {
- inherit: 'reddit.com',
- suffix: '/preview/external-pre',
- },
- },
-
- /**
- *
- * Below are less mature or partially featured services
- *
- */
-
- // stackoverflow.com, {subdomain}.stackexchange.com
- // superuser.com, serverfault.com, and other stack sites pending AnonymousOverflow support
- 'stackoverflow.com': {
- redirect: {
- replacements: [
- // 'https://ao.vern.cc', // low availability
- 'https://overflow.smnz.de',
- //'https://overflow.lunar.icu', // TODO: reenable, temp disable old version incompat w/ non-stackexchange sites
- 'https://overflow.adminforge.de',
- //'https://overflow.hostux.net', // low stability
- // 'https://overflow.projectsegfau.lt', // low availability
- ],
- },
- redirectToFarside: {
- replacements: ['https://farside.link/anonymousoverflow'],
- },
- },
- '.stackexchange.com': {
- allowFuzzy: true, // enable matching loosely with arbitrary subdomain
- redirect: {
- inherit: 'stackoverflow.com',
- routes: [
- {
- regex: /^https?:\/\/(www\.)?([a-z]+)\..*?\//g,
- suffix: '/exchange/$2/', // suffix /exchange/{subdomain} on hostname
- },
- ],
- },
- redirectToFarside: {
- inherit: 'stackoverflow.com',
- routes: [
- {
- regex: /^https?:\/\/(www\.)?([a-z]+)\..*?\//g,
- suffix: '/exchange/$2/',
- },
- ],
- },
- },
- // non-stackexchange domains are appended to route /exchange/{uri}
- 'superuser.com': {
- redirect: {
- inherit: 'stackoverflow.com',
- routes: [
- {
- regex: /^https?:\/\/(www\.)?(.*)/g, // take entire URI in group 2
- suffix: '/exchange/$2',
- },
- ],
- },
- redirectToFarSide: {
- inherit: 'stackoverflow.com',
- routes: [
- {
- regex: /^https?:\/\/(www\.)?(.*)/g, // take entire URI in group 2
- suffix: '/exchange/$2',
- },
- ],
- },
- },
- 'serverfault.com': { inherit: 'superuser.com' },
- 'askubuntu.com': { inherit: 'superuser.com' },
- 'stackapps.com': { inherit: 'superuser.com' },
-
- // quora.com
- 'quora.com': {
- redirect: {
- replacements: ['https://quetre.iket.me', 'https://quetre.pussthecat.org', 'https://quetre.privacydev.net'],
- },
- redirectToFarside: {
- replacements: ['https://farside.link/quetre'],
- },
- },
-
- // {artist}.bandcamp.com
- // Note: bandcamp.com/search route not supported, add above for 'bandcamp.com' if this rare link is needed is the wild
- // Note: {cdn}.bcbits.com routes not supported, add below for '.bcbits.com' if this rare link is needed in the wild
- '.bandcamp.com': {
- allowFuzzy: true,
- redirect: {
- replacements: ['https://tent.sny.sh', 'https://tn.vern.cc'],
- routes: [
- {
- // {artist}.bandcamp.com with no additional path except optional /music
- // exclude daily.bandcamp.com
- regex: /^https?:\/\/(www\.)?((?!daily\.)[a-z0-9\-]+)\.bandcamp\.com\/?(music)?$/g,
- suffix: '/artist.php?name=$2',
- },
- {
- // {artist}.bandcamp.com/{release}/{name}
- // exclude daily.bandcamp.com, e.g. daily.bandcamp.com/features/{article}
- regex: /^https?:\/\/(www\.)?((?!daily\.)[a-z0-9\-]+)\.bandcamp\.com\/([a-z]+)\/([a-z0-9\-]+)/g,
- suffix: '/release.php?artist=$2&type=$3&name=$4',
- },
- ],
- },
- },
-
- // instagram.com
- // Low feature parity
- 'instagram.com': {
- redirect: {
- replacements: ['https://ig.opnxng.com', 'https://proxigram.lunar.icu'],
- },
- redirectToFarside: {
- replacements: ['https://farside.link/proxigram'],
- },
- },
-
- // tiktok
- // Low feature parity
- 'tiktok.com': {
- redirect: {
- replacements: [
- 'https://proxitok.pussthecat.org',
- 'https://tok.artemislena.eu',
- 'https://tok.adminforge.de',
- 'https://tik.hostux.net',
- 'https://proxitok.lunar.icu',
- ],
- },
- redirectToFarside: {
- replacements: ['https://farside.link/proxitok'],
- },
- },
-
- // imgur.com, i.imgur.com, i.stack.imgur.com
- 'imgur.com': {
- redirect: {
- replacements: [
- 'https://rimgo.pussthecat.org',
- 'https://imgur.artemislena.eu',
- // 'https://rimgo.vern.cc', // low availability
- // 'https://rimgo.hostux.net', // down
- // 'https://rimgo.lunar.icu', // down
- 'https://rimgo.eu.projectsegfau.lt',
- ],
- },
- redirectToFarside: {
- replacements: ['https://farside.link/rimgo'],
- },
- },
- 'i.imgur.com': { inherit: 'imgur.com' },
- 'i.stack.imgur.com': {
- inherit: 'imgur.com',
- redirect: { inherit: 'imgur.com', suffix: '/stack' },
- redirectToFarside: { inherit: 'imgur.com', suffix: '/stack' },
- },
-
- // github.com, gists.github.com
- // /explore, /{group}/{repo}, /{group}/{repo}/archive, gists.github.com -> /gists/
- // Low feature parity
- // Use only for repo landing page, downloads, and gists
- 'github.com': {
- redirect: {
- replacements: [
- 'https://gothub.lunar.icu',
- 'https://g.opnxng.com',
- //'https://gothub.projectsegfau.lt', // low availability
- 'https://gothub.dev.projectsegfau.lt',
- ],
- },
- redirectToFarside: {
- replacements: ['https://farside.link/gothub'],
- },
- },
- // gist.github.com
- 'gist.github.com': {
- redirect: {
- inherit: 'github.com',
- suffix: '/gist/',
- routes: [{ regex: /https?:\/\/(.*?)\//g }], // replace entire domain
- },
- redirectToFarside: {
- inherit: 'github.com',
- suffix: '/gist/',
- routes: [{ regex: /https?:\/\/(.*?)\//g }],
- },
- },
-
- // imdb.com, m.imdb.com
- 'imdb.com': {
- redirect: {
- replacements: [
- 'https://libremdb.pussthecat.org',
- 'https://libremdb.iket.me',
- //'https://ld.vern.cc', // low availability
- 'https://libremdb.lunar.icu',
- ],
- },
- redirectToFarside: {
- replacements: ['https://farside.link/libremdb'],
- },
- },
- 'm.imdb.com': { inherit: 'imdb.com' },
-
- // genius.com
- // Low feature parity
- 'genius.com': {
- redirect: {
- replacements: ['https://dm.vern.cc', 'https://dumb.lunar.icu'],
- },
- redirectToFarside: {
- replacements: ['https://farside.link/dumb'],
- },
- },
-
- // medium.com - Uncomment to use
- // Low feature parity by design
- // Not a proxy by design, alternative frontend still requests from the official servers
- //
- // Recommend setting Medium to noscript and/or loading through more standard proxies such as TOR
- // Medium with JS disabled works as of now, but other proxy sites such as archive.org can be used if needed
- /* --- Remove this line to use --- //
- 'medium.com': {
- redirect: {
- replacements: ['https://scribe.rip', 'https://sc.vern.cc', 'https://m.opnxng.com'],
- },
- redirectToFarside: {
- replacements: ['https://farside.link/scribe'],
- },
- },
- // ------------------------------- */
-
- // fandom.com - Uncomment to use
- // Not a full proxy, alternative frontend still requests from the official servers
- //
- // Recommend simply using a content blocker to block ads and other annoyances
- /* --- Remove this line to use --- //
- '.fandom.com': {
- allowFuzzy: true,
- redirect: {
- replacements: [
- 'https://breezewiki.com',
- 'https://antifandom.com',
- 'https://breezewiki.pussthecat.org',
- 'https://bw.projectsegfau.lt',
- 'https://breeze.hostux.net',
- 'https://bw.artemislena.eu',
- 'https://breeze.nohost.network',
- 'https://z.opnxng.com',
- ],
- routes: [
- {
- regex: /^https?:\/\/(www\.)?([a-z\-]+)\..*?\//g,
- suffix: '/$2/', // suffix /{subdomain} on hostname
- },
- ],
- },
- redirectToFarside: {
- replacements: ['https://farside.link/breezewiki'],
- routes: [
- {
- regex: /^https?:\/\/(www\.)?([a-z\-]+)\..*?\//g,
- suffix: '/$2/',
- },
- ],
- },
- },
- // ------------------------------- */
-
- // wikipedia.org - Uncomment to use
- //
- // Recommend setting Wikipedia to noscript and/or loading through more standard proxies such as TOR
- // If absolutely needed, recommend rolling your own Wikiless instance routed through a proxy or VPN
- // Wikipedia trustworthiness and scriptless tracking is more or less equivalent to wikiless instances
- /* --- Remove this line to use --- //
- 'wikipedia.org': {
- redirect: {
- replacements: [
- 'https://wiki.adminforge.de',
- 'https://wikiless.lunar.icu',
- 'https://wikiless.org',
- 'https://wl.vern.cc',
- ],
- },
- redirectToFarside: {
- replacements: ['https://farside.link/wikiless'],
- },
- },
- // ------------------------------- */
- };
-
- /**
- * Configure site link exclusions
- *
- * The most common exclusion will be on the first-party site itself, as many proxies are not complete replacements.
- * Common unsuppoorted features and paths are excluded, though this is not intended to exhaustively track the list of
- * proxied frontends and availability or configuration of individual instances.
- *
- * excludeLinks can exclude links that match any provided rule, namely matchingPath
- * excludeOn can exclude links when host matches `self` or any provided, e.g. by matchingPath (all paths if empty)
- * @typedef {{
- * excludeLinks?: {
- * matchingPath?: (string|RegExp)[],
- * matchingHost?: (string|RegExp)[],
- * matchingUrl?: (string|RegExp)[],
- * matchingText?: (string|RegExp)[],
- * inherit?: string // inherit any undefined properties in `excludeLinks`
- * },
- * excludeOn?: {
- * matchingPath?: (string|RegExp)[],
- * matchingHost?: (string|RegExp)[],
- * matchingUrl?: (string|RegExp)[],
- * matchingBody?: (string|RegExp)[],
- * matchingHead?: (string|RegExp)[],
- * inherit?: string // inherit any undefined properties in `excludeOn`
- * },
- * allowFuzzy?: boolean, // allows fuzzy host matching, for subdomains
- * inherit?: string // inherits any undefined exclusion rules
- * }} Exclusion
- * @typedef {{ [host: string]: Exclusion }} ExclusionList
- */
-
- /**
- * Configure per-site exclusions
- * @type {ExclusionList}
- */
- const EXCLUSIONS = {
- // youtube.com, m.youtube.com, youtube-nocookie.com
- 'youtube.com': {
- excludeLinks: {
- matchingPath: ['/users/'],
- },
- excludeOn: {
- // exclude linking out from the official site
- matchingHost: ['youtube.com', 'youtube-nocookie.com'],
- matchingHead: [
- '<meta property="og:title" content="Piped">',
- /<meta property="og:site_name" content=".*Invidious">/,
- ],
- },
- },
- 'm.youtube.com': { inherit: 'youtube.com' },
- 'youtube-nocookie.com': { inherit: 'youtube.com' },
-
- // reddit.com
- 'reddit.com': {
- excludeLinks: {
- // old.reddit.com can be used almost entirely with noscript
- matchingHost: ['old.reddit.com'],
- },
- excludeOn: {
- // exclude on reddit.com but still proxy links while browsing old.reddit.com
- matchingUrl: [/^https?:\/\/(www\.)?(?!old\.)reddit.com/],
- },
- },
-
- // stackoverflow.com, {subdomain}.stackexchange.com, superuser.com, etc.
- 'stackoverflow.com': {
- excludeLinks: {
- matchingPath: ['/questions/tagged/', '/users/'],
- },
- excludeOn: {
- matchingHost: [
- 'stackoverflow.com',
- 'stackexchange.com',
- 'superuser.com',
- 'serverfault.com',
- 'askubuntu.com',
- 'stackapps.com',
- ],
- },
- },
- '.stackexchange.com': {
- allowFuzzy: true,
- inherit: 'stackoverflow.com',
- },
- 'superuser.com': { inherit: 'stackoverflow.com' },
- 'serverfault.com': { inherit: 'stackoverflow.com' },
- 'askubuntu.com': { inherit: 'stackoverflow.com' },
- 'stackapps.com': { inherit: 'stackoverflow.com' },
-
- // quora.com
- 'quora.com': { excludeOn: { matchingHost: ['quora.com'] } },
-
- // {artist}.bandcamp.com
- '.bandcamp.com': { allowFuzzy: true, excludeOn: { matchingHost: ['bandcamp.com'] } },
-
- // instagram.com
- 'instagram.com': {
- excludeOn: {
- matchingHost: ['instagram.com'],
- matchingHead: [
- /<meta property="og:title" content="[a-zA-Z0-9 _\-+=.,:;'?\/\\`!@#$%^&*()-_\[\]{}|]+? • Proxigram">/,
- ],
- },
- },
-
- // tiktok.com
- 'tiktok.com': {
- excludeOn: {
- matchingHost: ['tiktok.com'],
- // exclude on ProxiTok itself, linking out to original
- matchingHead: ['<meta property="og:site_name" content="ProxiTok">'],
- },
- },
-
- // imgur.com
- 'imgur.com': { excludeOn: { matchingHost: ['imgur.com'] } },
- 'i.imgur.com': { inherit: 'imgur.com' },
- 'i.stack.imgur.com': { inherit: 'imgur.com' },
-
- // github.com
- // a lot to blacklist, can also whitelist limited functionality instead
- 'github.com': {
- excludeLinks: {
- matchingText: [],
- // prettier-ignore
- matchingPath: ['/actions','/blame/','/codespaces/','/collections/','/commit/','/commits/','/compare/','/customer-stories','/delete/','/discussions/','/enterprise/','/events/','/features/','/graphs/','/issues','/marketplace/','/notifications/','/orgs/','/projects/','/pulls', '/pull/','/pulse','/releases','/security','/sessions/','/sponsors/','/tags','/tree/','/wiki/'],
- },
- excludeOn: {
- matchingHost: ['github.com'],
- // exclude on Gothub page itself, linking out
- matchingBody: ['<a href="https://codeberg.org/gothub/gothub">Source code</a>'],
- },
- },
- 'gist.github.com': {
- excludeLinks: {
- // gist single directory paths, e.g. users, /discover, /starred
- matchingPath: [/^\/[A-Za-z0-9_.-]+\/?$/],
- },
- inherit: 'github.com',
- },
-
- // imdb.com
- 'imdb.com': {
- excludeOn: {
- matchingHost: ['imdb.com'],
- // exclude on libremdb itself, linking out
- matchingHead: ['<meta property="og:site_name" content="libremdb">'],
- },
- },
-
- // genius.com
- // /artist and other pages may not work, but not blacklisting any paths for now
- 'genius.com': { excludeOn: { matchingHost: ['genius.com'] } },
-
- // medium.com
- 'medium.com': { excludeOn: { matchingHost: ['medium.com'] } },
-
- // fandom.com
- 'fandom.com': { excludeOn: { matchingHost: ['fandom.com'] } },
-
- // wikipedia.org
- 'wikipedia.org': { excludeOn: { matchingHost: ['wikipedia.org'] } },
- };
-
- /**
- * Begin script logic
- *
- * Do not modify below if adding or making changes to available proxies
- */
- const DEBUG = true; // Console logging enabled when true
-
- // Tag names of hovered link elements that can be proxified
- const PROXIFY_ON = ['A', 'IFRAME'];
- // Tag names of hovered elements that should also lookup to the parent node for links
- // TODO: per-tag and per-site configuration
- const PARENT_LOOKUP_ON = [
- 'BUTTON',
- 'IMG',
- 'SVG',
- 'H1',
- 'H2',
- 'H3',
- 'H4',
- 'H5',
- 'H6',
- 'B',
- 'I',
- 'EM',
- 'STRONG',
- 'SMALL',
- 'SUP',
- 'SUB',
- 'S',
- 'U',
- 'LI',
- ];
- // Default depth limit to ancestor lookup recursion
- const PARENT_LOOKUP_STEPS = 1;
- /**
- * Additional per-site parent lookup config, to fix site-specific proxification
- * TODO: further segment sites on matchesPath or other rules and per-nodeName configs
- * @type {{ [host: string]: { nodeNames: string[], steps: number } }}
- */
- const PARENT_LOOKUP_ON_SITE = {
- // google.com mobile search results div
- // TODO: improve mobile search inline media touch events swallowed
- 'google.com': { nodeNames: ['DIV'], steps: 4 },
- // duckduckgo.com search results span, hero summary SVGs and title container div
- 'duckduckgo.com': { nodeNames: ['SPAN', 'DIV', 'PATH', 'RECT'], steps: 4 },
- // startpage.com search results inline images/videos
- 'startpage.com': { nodeNames: ['DIV'], steps: 2 },
- };
-
- // Tag names of hovered elements that should also globally search all hovered elements for links
- const HOVER_LOOKUP_ON = ['P', 'SPAN', 'LI', 'LABEL', /*'DIV',*/ 'BUTTON', 'IMG', 'SVG'];
- // Additional tag names to search per-site, to fix site-specific proxificaton
- /** @type {{ [host: string]: [tags: string[]] }} */
- const HOVER_LOOKUP_ON_HOST = {
- 'duckduckgo.com': ['DIV'],
- };
-
- const CLASS_PROXIFIED = 'proxi-fied';
- const CLASS_PROXIFIED_LIVE = 'proxi-live';
- const CLASS_BODY_FARSIDE = 'proxi-side';
- const CLASS_BODY_BONAFIDE = 'proxi-fide';
- const CLASS_BODY_BITE = 'proxi-bite';
- const CLASS_BODY_FRAMEBITE = 'proxi-framebite';
- const STYLE_HIGHLIGHT_ID = 'proxi-highlite';
- const ATTR_PROXIFIED_SITE = 'proxi-site';
- const ATTR_PROXIFIED_FARSIDE = 'proxi-site-side';
- const ATTR_PROXIFIED_DENIED = 'proxi-nied';
- const ATTR_BONAFIDE_SITE = 'proxi-site-bonafide';
- const ATTR_IS_FARSIDE = 'proxi-side';
-
- // https://uibakery.io/regex-library/url
- const REGEX_URL =
- /^https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_\+.~#?&\/=]*)$/;
-
- // Key event modifiers for selecting destination
- document.body.addEventListener('keydown', e => handleModifierKey(e));
- document.body.addEventListener('keyup', e => handleModifierKey(e));
- document.body.addEventListener('touchstart', e => handleModifierGesture(e));
-
- /**
- * Lazy load link processing when user interacts per-element
- *
- * Handle hovering over links and completed keyboard navigation over link
- * Does not handle other basic redirects such as form action or onclick attributes
- */
- document.body.addEventListener('mouseover', handleElement); // link hover
- document.body.addEventListener('touchstart', handleElement); // link touch
- document.body.addEventListener('keyup', handleElement); // keyboard navigation
-
- /**
- * Automatic page onload and DOM modifications
- */
- window.addEventListener('load', () => {
- if (ENABLE_SEARCH_GET) {
- let searchEngine = getByHost(ENABLE_SEARCH_GET_ON_SITE, window.location.host, false);
- if (!!searchEngine && !!searchEngine.formSelector) {
- const searchEngineFormEls = document.querySelectorAll(searchEngine.formSelector);
- for (let formEl of searchEngineFormEls) {
- formEl.method = 'get';
- }
- }
- }
- });
-
- /**
- * Hover event handler to find an anchor hyperlink to check
- * @param {Event} e
- */
- function handleElement(e) {
- // Currently support specific events, and optional key nav filter
- if (
- e.type !== 'mouseover' &&
- e.type !== 'touchstart' &&
- (e.type !== 'keyup' || (!!KEY_NAVIGATE && e.key !== KEY_NAVIGATE))
- ) {
- return;
- }
-
- // Ignore target on multi-touch touch event
- if (e.type === 'touchstart' && e.touches.length !== 1) {
- return;
- }
-
- // Look for any element or elements that this event could be trying to proxify
- const proxifiableEls = getProxifiableElements(e);
- if (!proxifiableEls || !proxifiableEls.length) return;
-
- // Prioritize anchor links
- // Take the first anchor, even if it has already been processed
- const targetAnchor = proxifiableEls.find(el => el.nodeName === 'A');
- if (!!targetAnchor) {
- // Hovered anchor found, process it unless it was already processed before
- if (!isElementProxified(targetAnchor)) {
- handleAnchorEl(targetAnchor);
- } else {
- // Otherwise, trigger an update on the link element to ensure it is current
- setTimeout(() => {
- updateProxifiedElement(targetAnchor, 'href');
- }, 0);
- }
-
- // Only handle one element per event for now to avoid overeager proxification
- return;
- }
-
- // Handle iframe, if enabled
- // If both or either bite key is empty, handle on any interaction
- if (ENABLE_IFRAME_PROXIFIED) {
- const isProxibite = document.body.classList.contains(CLASS_BODY_BITE);
- const isFramebite = document.body.classList.contains(CLASS_BODY_FRAMEBITE);
- const canBite =
- (!KEY_PROXIBITE || isProxibite || e.key === KEY_PROXIBITE || e.touches?.length === TOUCH_PROXIBITE) &&
- (!KEY_FRAMEBITE || isFramebite || e.key === KEY_FRAMEBITE || e.touches?.length === TOUCH_FRAMEBITE);
- const targetIframe = canBite && proxifiableEls.find(el => el.nodeName === 'IFRAME');
- if (!!targetIframe) {
- if (!isElementProxified(targetIframe)) {
- handleIframe(targetIframe);
- } else {
- setTimeout(() => {
- updateProxifiedElement(targetIframe, 'src');
- }, 0);
- }
- }
-
- return;
- }
- }
-
- /**
- * Handle an anchor element to be proxified
- * Allows re-proxifying the anchor
- *
- * @param {HTMLAnchorElement} el Anchor element
- * @returns {void}
- */
- function handleAnchorEl(el) {
- // Perform element and url validation and returns a valid proxy
- const { url, proxy } = preproxifyElement(el, 'href') || {};
-
- // Perform any global link modifications including non-proxied links
- if (ENABLE_REFERER_HIDE && ENABLE_REFERER_HIDE_PAGEWIDE) el.rel = 'noreferrer';
- let optSmite;
- if (
- ENABLE_ATTRIBUTES_SMITE &&
- !!el.href &&
- !!(optSmite = getByHost(ENABLE_ATTRIBUTES_SMITE_ON_SITE, window.location.host, true))
- ) {
- // If enabled on the current site, strip any click event attributes from this anchor element
- // Resulting hyperlink is intended to be a primitive link with no events intercepting navigation
- // TODO: Support reverting events on bonafide
- for (const attr of optSmite.attributes) {
- el.removeAttribute(attr);
- }
- }
-
- // Proxify the link
- // Error handling will have been done on preproxification
- if (!!proxy) {
- proxifyElement(
- el,
- url,
- 'href',
- proxy,
- /** @param {HTMLAnchorElement} el */ el => {
- // Trigger an update on the link now that it has been proxified
- updateProxifiedElement(el, 'href');
-
- // Upgrade undefined or noopener relationship to norefererer
- // If `ENABLE_REFERER_HIDE`, override on all proxified links
- // TODO: Support modifier key to revert change?
- if (!el.rel || el.rel === 'noopener' || ENABLE_REFERER_HIDE) el.rel = 'noreferrer';
- }
- );
- }
- }
-
- /**
- * Handle an iframe to be proxified
- * Allows re-proxifying the frame
- *
- * @param {HTMLIFrameElement} el
- * @returns {void}
- */
- function handleIframe(el) {
- const { url, proxy } = preproxifyElement(el, 'src') || {};
- if (!!proxy)
- proxifyElement(
- el,
- url,
- 'src',
- proxy,
- /** @param {HTMLIframeElement} el */ el => {
- // Trigger an update on the link now that it has been proxified
- updateProxifiedElement(el, 'src');
- }
- );
- }
-
- /**
- * Get the element or elements that can be proxified, including those already proxified,
- * from an event where the user is hovering or making a selection
- *
- * @param {MouseEvent | KeyboardEvent | TouchEvent} e
- * @returns {Element[]?} If none, null
- */
- function getProxifiableElements(e) {
- if (!(e instanceof MouseEvent || e instanceof KeyboardEvent || e instanceof TouchEvent)) {
- error('Invalid event');
- return null;
- }
-
- if (!e.target) return null;
- const nodeName = e.target.nodeName.toUpperCase();
-
- // Anchor links get top priority if directly targeted
- if (nodeName === 'A') return [e.target];
-
- // Otherwise, build a list of candidates in the order of probable priority
- // Add in other directly targeted non-anchor links to the top
- const proxifiableEls = [];
- if (PROXIFY_ON.includes(nodeName)) proxifiableEls.push(e.target);
-
- // Parents may not receive propagated events, so use this event now to check if nested under a link
- let optLookup = getByHost(PARENT_LOOKUP_ON_SITE, window.location.host, true);
- if (PARENT_LOOKUP_ON.includes(nodeName) || optLookup?.nodeNames?.includes(nodeName)) {
- // Step up through ancestors
- // Or use target.closest() to search all ancestors
- const lookupSteps = Math.max(optLookup?.steps ? optLookup.steps : 1, PARENT_LOOKUP_STEPS);
- let currentNode = e.target;
- for (let step = 0; step < lookupSteps; step++) {
- if (!!currentNode.parentNode) {
- currentNode = currentNode.parentNode;
-
- // take processed or unprocessed proxifiable elements
- if (PROXIFY_ON.includes(currentNode.nodeName.toUpperCase())) {
- proxifiableEls.push(currentNode);
- }
- } else {
- break;
- }
- }
- }
-
- // A link may be a sibling or other relative, or not even a relative, that does not receive propagated
- // For mouse hover or bite event, find any link element that is currently being hovered
- // TODO: Skip previously lookup elements and handle updateProxifiedElement rerender separately from proxifying
-
- const isHoverLookup =
- e instanceof MouseEvent &&
- (HOVER_LOOKUP_ON.includes(nodeName) || HOVER_LOOKUP_ON_HOST[window.location.host]?.includes(nodeName));
- const isProxiBite = !!KEY_PROXIBITE && document.body.classList.contains(CLASS_BODY_BITE);
- if (isHoverLookup || isProxiBite) {
- // Select all hovered elements and take the processed or unprocessed proxifiable elements
- const hoveredEls = document.querySelectorAll(':hover');
- for (const el of hoveredEls) {
- if (PROXIFY_ON.includes(el.nodeName.toUpperCase())) {
- proxifiableEls.push(el);
- }
- }
- }
-
- return !!proxifiableEls.length ? proxifiableEls : null;
- }
-
- /**
- * Perform element and destination url validation and return a valid proxy
- *
- * @param {HTMLElement} el Target element to be validated for proxification
- * @param {string} destinationAttr Element link destination attribute name
- * @returns {{url, Proxy} | null} Returns a random proxy for the element, null if none
- */
- function preproxifyElement(el, destinationAttr) {
- const destination = el[destinationAttr];
- const text = el.outerText;
-
- // Validate anchor is intended to be a hyperlink
- if (!destination.length) {
- proxifyDenyElement(el, 'Invalid, empty, or undefined destination attribute');
- return null;
- }
- if (!REGEX_URL.test(destination)) {
- proxifyDenyElement(el, 'Hyperlink destination is not a valid absolute URL');
- return null;
- }
-
- // Clean up any proxify attrs before proxifying
- el.removeAttribute(ATTR_PROXIFIED_SITE);
- el.removeAttribute(ATTR_PROXIFIED_FARSIDE);
- el.removeAttribute(ATTR_PROXIFIED_DENIED);
- el.removeAttribute(ATTR_BONAFIDE_SITE);
-
- // Parse the destination as a URL
- let url;
- try {
- url = new URL(destination);
- } catch (ex) {
- // Exit on malformed URL
- error(`[${text}](${destination}) error parsing URL: ${ex}`);
- proxifyDenyElement(el, 'Error parsing URL');
- return null;
- }
-
- // Exit if the original link is excluded for any reason
- // Page-wide exclusions to each destination host are memoized so may save unnecessary checking
- let excludedReason;
- if ((excludedReason = getExclusionReasonForLink(url, text))) {
- proxifyDenyElement(el, excludedReason);
- return null;
- }
-
- // Get a flattened proxy rule that can be applied for this link
- let proxy = getProxyForLink(url);
-
- // If no proxy can be found for the link url, optionally search its query string
- // Find a proxy match for any query param that is whitelisted, if used, and is a valid url
- if (!proxy && ENABLE_QUERY_PROXIFIED) {
- for (const [paramKey, paramVal] of url.searchParams) {
- // skip blacklisted query params
- if (ENABLE_QUERY_PROXIFIED_OFF.includes(paramKey)) {
- continue;
- }
-
- // if query param whitelist is empty, allow all
- if (!ENABLE_QUERY_PROXIFIED_ON?.length || ENABLE_QUERY_PROXIFIED_ON.includes(paramKey)) {
- let queryStringLinkCandidate = paramVal;
- let queryStringURLCandidate, queryStringProxyCandidate;
- try {
- queryStringLinkCandidate = decodeURI(paramVal); // attempt to decode a full URI encoded to the query string
- } catch (ex) {
- warn(`Failed to decode query string param: ${ex}`);
- continue;
- }
-
- // skip param if it doesn't look like an absolute url to proxify
- if (!REGEX_URL.test(queryStringLinkCandidate)) {
- continue;
- }
-
- try {
- queryStringURLCandidate = new URL(queryStringLinkCandidate);
- } catch (ex) {
- error(`Failed to parse validated url for query param "${paramKey}"`);
- continue;
- }
-
- if (getExclusionReasonForLink(queryStringURLCandidate, text)) {
- continue;
- }
-
- queryStringProxyCandidate = getProxyForLink(queryStringURLCandidate);
-
- // if successful, break on query string and take first match
- if (!!queryStringProxyCandidate) {
- url = queryStringURLCandidate;
- proxy = queryStringProxyCandidate;
- break;
- }
- }
- }
- }
-
- // Automatic exclusion if no proxy found on this pass
- if (!proxy) {
- proxifyDenyElement(el, 'Not found in proxies list');
- return null;
- }
-
- return { url, proxy };
- }
-
- /**
- * Proxify the link element with the specified proxy settings
- * Supports anchor and iframe elements for now
- *
- * @param {HTMLAnchorElement | HTMLIFrameElement} el
- * @param {URL} url
- * @param {string} destinationAttr Element link destination attribute name
- * @param {Proxy} proxy
- * @param {Function<HTMLAnchorElement | HTMLIFrameElement>?} onProxification Optional success callback
- * @returns {void}
- */
- function proxifyElement(el, url, destinationAttr, proxy, onProxification) {
- if (!(el instanceof HTMLAnchorElement) && !(el instanceof HTMLIFrameElement)) {
- error(`${el} is not a supported element type`);
- return;
- }
-
- if (!isProxyValid(proxy)) {
- error(`Invalid proxy settings for proxifying ${url}`);
- return;
- }
-
- let proxifyDenied;
-
- // Build a url to a random instance
- if (!!proxy.redirect) {
- const instanceList = proxy.redirect.replacements;
- const instanceSuffix = proxy.redirect.suffix;
- const instanceRoutes = proxy.redirect.routes;
- const urlInstanceProxified = getProxifiedUrl(url, instanceList, instanceSuffix, instanceRoutes, 'proxy');
-
- if (urlInstanceProxified instanceof URL) {
- el.setAttribute(ATTR_PROXIFIED_SITE, urlInstanceProxified);
- } else if (typeof urlInstanceProxified === 'string') {
- proxifyDenied ||= urlInstanceProxified;
- }
- }
-
- // Build a url to a random Farside service redirect
- if (!!proxy.redirectToFarside) {
- const farsideList = proxy.redirectToFarside.replacements;
- const farsideSuffix = proxy.redirectToFarside.suffix;
- const farsideRoutes = proxy.redirectToFarside.routes;
- const urlFarsideProxified = getProxifiedUrl(url, farsideList, farsideSuffix, farsideRoutes, 'farside');
-
- if (urlFarsideProxified instanceof URL) {
- el.setAttribute(ATTR_PROXIFIED_FARSIDE, urlFarsideProxified);
- } else if (typeof urlFarsideProxified === 'string') {
- proxifyDenied ||= urlFarsideProxified;
- }
- }
-
- // Complete the proxification
- if (el.hasAttribute(ATTR_PROXIFIED_SITE) || el.hasAttribute(ATTR_PROXIFIED_FARSIDE)) {
- // Mark the link as successfully proxified
- el.classList.add(CLASS_PROXIFIED);
-
- // Store the original anchor href
- if (!el.hasAttribute(ATTR_BONAFIDE_SITE)) {
- el.setAttribute(ATTR_BONAFIDE_SITE, el[destinationAttr]);
- }
-
- // Success callback
- if (!!onProxification) {
- onProxification(el);
- }
- } else if (!!proxifyDenied) {
- // No effect proxifying, but a denial reason was given
- proxifyDenyElement(el, proxifyDenied);
- } else {
- // Log unexpected failure past validated inputs
- error(`Failure proxifying ${url}`);
- }
- }
-
- /**
- * Process but mark the link element as being invalid
- *
- * @param {HTMLAnchorElement | HTMLIFrameElement} el
- * @param {string} reason
- */
- function proxifyDenyElement(el, reason) {
- if (!(el instanceof HTMLAnchorElement) && !(el instanceof HTMLIFrameElement)) {
- error(`${el} is not a supported element type`);
- return;
- }
-
- // Set denial reason as attr value
- el.setAttribute(ATTR_PROXIFIED_DENIED, reason);
-
- // Bonafide link is still stored for posterity
- el.setAttribute(ATTR_BONAFIDE_SITE, getElementDest(el));
- }
-
- /**
- * Return a proxified URL given a list of replacement hosts and optional routes
- *
- * @param {URL} url
- * @param {string[]} replacements
- * @param {string?} suffix Optional static suffix on the replacement host
- * @param {ProxyRoute[]?} routes Optional route list specifying a regex match with its corresponding options
- * @param {string} key Optional unique key used for proxifying
- * @returns {URL | string | null} Proxified URL, string reason if denied, null if failed
- */
- const replacementLast = {};
- function getProxifiedUrl(url, replacements, suffix, routes, key = '') {
- if (!url) {
- error('URL null or undefined');
- return null;
- }
-
- if (!replacements) {
- error('Replacements list null or undefined');
- return null;
- }
-
- // Get a cleaned list of replacement strings
- const replacementList = replacements.filter(
- replacement => typeof replacement === 'string' && REGEX_URL.test(replacement)
- );
-
- // Proxify the url with one of the replacements
- if (replacementList.length > 0) {
- // get a random replacement site
- const hostKey = url.host + key;
- const replacementIndex = (replacementLast[hostKey] =
- ROTATE_SITE_PROXIFIED && Object.keys(replacementLast).includes(hostKey)
- ? (replacementLast[hostKey] + 1) % replacementList.length // rotate proxies sequentially, if enabled
- : Math.floor(Math.random() * replacementList.length));
-
- const replacementSite = replacementList[replacementIndex];
- const replacementSuffix = suffix || '';
- const replacementRoute = routes?.find(route => route?.regex instanceof RegExp && route.regex.test(url.href));
-
- const failedWhitelist = !!routes && !replacementRoute; // proxy routes whitelist defined but none matched
- let urlProxified = new URL(url);
- if (!!replacementRoute) {
- // Regex replacement
- // TODO: Support replacement function as an alternative to the replacement string route suffix
- try {
- const routeRegex = replacementRoute.regex;
- const routeSuffix = replacementRoute.suffix || '';
- const hrefProxified = url.href.replace(routeRegex, replacementSite + replacementSuffix + routeSuffix);
- urlProxified = new URL(hrefProxified);
- } catch (ex) {
- warn(`Invalid regex replaced URL for ${url}`);
- return null;
- }
- } else if (!failedWhitelist) {
- // Default regex replacement on url host and scheme
- // Skip if whitelisted routes were defined but not matched
- try {
- const hrefProxified = url.href.replace(/(https?:\/\/)(.*?)(\/.*)/, replacementSite + replacementSuffix + '$3');
- urlProxified = new URL(hrefProxified);
- } catch (ex) {
- error(`Invalid default replaced URL for ${url}`);
- return null;
- }
- }
-
- if (url.href !== urlProxified.href) {
- return urlProxified;
- } else if (failedWhitelist) {
- return 'Failed routes whitelist';
- } else {
- error(`No effect proxifying ${url}`);
- return null;
- }
- }
-
- warn(`Missing or invalid replacement URLs for ${url}`);
- return null;
- }
-
- /**
- * Returns matching proxy for the specified link url
- *
- * @param {URL} url
- * @returns {Proxy?} Proxy settings by url, null if not found
- */
- function getProxyForLink(url) {
- // Get the redirect rules from the proxy matching url host
- /** @type {Proxy?} */
- const proxy = getByHost(PROXIES, url.host, true);
-
- // Inherit any redirect rules that were left completely undefined, then inherit
- // any rule properties that were left undefined
- /** @type {Proxy?} */
- let flattenedProxyRule;
- try {
- flattenedProxyRule = flattenInheritance(proxy, PROXIES);
- } catch (ex) {
- error(`Error inheriting proxy rules for ${url}: ${ex}`);
- return false;
- }
-
- if (!!flattenedProxyRule) {
- return flattenedProxyRule;
- }
-
- return null;
- }
-
- /**
- * Returns whether link URL is supported by a listed proxy and not explicitly excluded
- *
- * @param {URL} url
- * @param {string} text
- * @returns {string|null} Reason if link is invalid, null if valid
- */
- function getExclusionReasonForLink(url, text) {
- // Exit if this URL is excluded by path or innertext, or by the current page location
- // Look for exclusion rules on both this host and inherited, if applicable
- /** @type {Exclusion?} */
- const exclusion = getByHost(EXCLUSIONS, url.host, true);
-
- /** @type {Exclusion?} */
- let flattenedExclusionRule;
- try {
- flattenedExclusionRule = flattenInheritance(exclusion, EXCLUSIONS);
- } catch (ex) {
- // Likely inheriting rule that is unexpectedly undefined, probably due to incorrect or nested inheritance
- warn(`Error inheriting exclusion rules for ${url}: ${ex}`);
- return 'Error inheriting exclusion rule';
- }
-
- // With the final exclusion rule set, check if the link url is excluded
- if (!!flattenedExclusionRule) {
- const excludedReason = isLinkExcludedByRule(url, text, flattenedExclusionRule);
- if (!!excludedReason) {
- return excludedReason;
- }
- }
-
- return null;
- }
-
- /**
- * Flatten generic object upwards with inherited data
- *
- * Only goes one level deep, both for inheritance and nested inheritors
- * @typedef {{
- * [keys: string]: Inheritor,
- * inherit?: string
- * }} Inheritor
- *
- * @param {Inheritor} source
- * @param {Object<string, Inheritor>} dictionary
- * @returns {Inheritor?}
- * @throws {Error} Exception on copying with Object.assign()
- */
- function flattenInheritance(source, dictionary) {
- // Avoid destructive shallow copies on `source` or other objects
- let root = source;
- if (!root || !(typeof root === 'object')) return null;
-
- const rootInheritance = dictionary[root.inherit];
- if (!!rootInheritance && typeof rootInheritance === 'object') {
- // Merge two rules into one
- root = Object.assign({}, rootInheritance, root);
- }
-
- // For the final state of inherited nested objects that also inherit,
- // flatten their inheritance as well (non-recursive)
- for (const key of Object.keys(root)) {
- const branch = root[key];
- if (branch?.inherit && dictionary[branch.inherit]) {
- // if inherited object also includes corresponding nested data, merge to
- const branchInheritance = dictionary[branch.inherit]?.[key];
- if (!!branchInheritance && typeof branchInheritance === 'object') {
- root[key] = Object.assign({}, branchInheritance, branch);
- }
- }
- }
-
- // Warn if it appears that recursive inheritance is configured
- // Inheritance beyond the first level of root and property data is not supported
- if (!!rootInheritance?.inherit) {
- warn(`Multi depth recursion hit ${rootInheritance.inherit} but is not supported`);
- }
-
- return root;
- }
-
- /**
- * Validate proxy rules have enough instance or Farside rules to proxify a link
- *
- * @param {Proxy} proxy
- * @returns {boolean} true if valid, false otherwise
- */
- function isProxyValid(proxy) {
- if (!proxy) return false;
-
- const isInstanceRedirectValid =
- !!proxy.redirect && proxy.redirect.replacements && proxy.redirect.replacements.length > 0;
- const isFarsideRedirectValid =
- !!proxy.redirectToFarside &&
- proxy.redirectToFarside.replacements &&
- proxy.redirectToFarside.replacements.length > 0;
- if (!isInstanceRedirectValid && !isFarsideRedirectValid) {
- // neither instance or Farside rules fully defined
- return false;
- }
-
- return true;
- }
-
- /**
- * Handle keydown and keyup event to set modifiers on proxified links
- * @param {KeyboardEvent} e
- * @returns {void}
- */
- function handleModifierKey(e) {
- if (e.type !== 'keyup' && e.type !== 'keydown') {
- error('Invalid modifier key event');
- return;
- }
-
- let isDomChanged = false;
- toggleModifierKeyState(e, KEY_MODIFIED, CLASS_BODY_FARSIDE) && (isDomChanged = true);
- toggleModifierKeyState(e, KEY_BONAFIDE, CLASS_BODY_BONAFIDE) && (isDomChanged = true);
- toggleModifierKeyState(e, KEY_PROXIBITE, CLASS_BODY_BITE) && (isDomChanged = true);
- toggleModifierKeyState(e, KEY_FRAMEBITE, CLASS_BODY_FRAMEBITE) && (isDomChanged = true);
-
- // If DOM state changed, trigger an render update on proxified elements
- if (isDomChanged) {
- setTimeout(() => {
- updateAllProxifiedAnchors(true);
- }, 0);
- }
- }
-
- /**
- * Handle multi-touch gesture event to set modifiers on proxified links, for touch-only devices
- * @param {TouchEvent} e
- * @returns {void}
- */
- function handleModifierGesture(e) {
- if (!(e instanceof TouchEvent)) {
- error('Invalid modifier touch gesture event');
- return;
- }
-
- // only support multi-touch to trigger/reset modifiers
- if (e.touches.length <= 1) {
- return;
- }
-
- let isDomChanged = false;
- toggleModifierGestureState(e, TOUCH_MODIFIED, CLASS_BODY_FARSIDE) && (isDomChanged = true);
- toggleModifierGestureState(e, TOUCH_BONAFIDE, CLASS_BODY_BONAFIDE) && (isDomChanged = true);
- toggleModifierGestureState(e, TOUCH_PROXIBITE, CLASS_BODY_BITE, true) && (isDomChanged = true);
- toggleModifierGestureState(e, TOUCH_FRAMEBITE, CLASS_BODY_FRAMEBITE, true) && (isDomChanged = true);
-
- // If DOM state changed, trigger an render update on proxified elements
- if (isDomChanged) {
- setTimeout(() => {
- updateAllProxifiedAnchors(true);
- }, 0);
- }
- }
-
- /**
- * Toggle a page-wide class by checking KeyboardEvent matches the specified toggleKey
- *
- * @param {KeyboardEvent} e
- * @param {string} toggleKey
- * @param {string} toggleClass Classname to toggle in DOM
- * @returns {boolean} True if changed, false if no change to DOM
- */
- function toggleModifierKeyState(e, toggleKey, toggleClass) {
- if (!(e instanceof KeyboardEvent)) {
- error('Event is not KeyboardEvent');
- return;
- }
-
- if (e.type !== 'keyup' && e.type !== 'keydown') {
- error('Invalid modifier key event');
- return false;
- }
-
- if (e.key === toggleKey) {
- const setModifierOn = e.type === 'keydown';
- const originalState = document.body.classList.contains(toggleClass);
-
- if (setModifierOn) {
- if (!originalState) {
- document.body.classList.add(toggleClass);
- return true;
- }
- } else {
- if (originalState) {
- document.body.classList.remove(toggleClass);
- return true;
- }
- }
- }
-
- return false;
- }
-
- /**
- * Toggle a page-wide class by checking TouchEvent matches the specified touch gesture
- *
- * @param {ToggleEvent} e
- * @param {number} toggleGesture Simple gesture based on number of fingers
- * @param {string} toggleClass Classname to toggle in DOM
- * @param {boolean} isManualOff Optional setting to require manually repeating gesture to toggle off
- * @returns {boolean} True if changed, false if no change to DOM
- */
- function toggleModifierGestureState(e, toggleGesture, toggleClass, isManualOff = false) {
- if (!(e instanceof TouchEvent)) {
- error('Event is not TouchEvent');
- return;
- }
-
- // detect gesture
- // only simple touch count gesture is supported currently
- const gestureTouchCount = toggleGesture;
- const isGestured = gestureTouchCount !== TOUCH_PROXIFIED && e.touches.length === gestureTouchCount;
-
- // handle gesture to toggle modifier
- const originalState = document.body.classList.contains(toggleClass);
- if (isGestured) {
- if (!originalState) {
- document.body.classList.add(toggleClass);
- return true;
- } else if (isManualOff) {
- // gesture off, if supported by this gesture
- document.body.classList.remove(toggleClass);
- return true;
- }
- } else if (!isManualOff) {
- // automatic off, if supported by this gesture
- if (originalState) {
- document.body.classList.remove(toggleClass);
- return true;
- }
- }
-
- return false;
- }
-
- /**
- * Rerender proxified changes to the specified link element
- *
- * @param {HTMLElement} el
- * @param {string} destinationAttr Element link destination attribute name
- * @returns {void}
- */
- function updateProxifiedElement(el, destinationAttr) {
- if (!(el instanceof HTMLElement)) {
- error(`${el} is not an HTMLElement`);
- return;
- }
-
- if (el.hasAttribute(ATTR_PROXIFIED_DENIED)) {
- return; // element has been proxified but denied
- }
-
- if (!el[destinationAttr]) {
- error(`Invalid attribute ${destinationAttr}`);
- return;
- }
-
- if (!isElementProxified(el)) {
- error(`Anchor [${destinationAttr}=${el[destinationAttr]}] is not proxified`);
- return;
- }
-
- // Check DOM modifier state
- // Instead of using internal state, follow what the rendered DOM has
- const isBonafide = document.body.classList.contains(CLASS_BODY_BONAFIDE);
- // attempt to use Farside automatically if direct site is not found
- const isFarside = document.body.classList.contains(CLASS_BODY_FARSIDE) || !el.hasAttribute(ATTR_PROXIFIED_SITE);
-
- // Reset active states before re-applying as necessary
- el.removeAttribute(ATTR_IS_FARSIDE);
- el.classList.remove(CLASS_PROXIFIED_LIVE);
-
- // Update link destination
- // The original, bonafide link takes precedence
- if (isBonafide && el.hasAttribute(ATTR_BONAFIDE_SITE)) {
- if (el[destinationAttr] !== el.getAttribute(ATTR_BONAFIDE_SITE))
- el[destinationAttr] = el.getAttribute(ATTR_BONAFIDE_SITE);
- } else if (isFarside && el.hasAttribute(ATTR_PROXIFIED_FARSIDE)) {
- if (el[destinationAttr] !== el.getAttribute(ATTR_PROXIFIED_FARSIDE))
- el[destinationAttr] = el.getAttribute(ATTR_PROXIFIED_FARSIDE);
- el.setAttribute(ATTR_IS_FARSIDE, true);
- el.classList.add(CLASS_PROXIFIED_LIVE);
- } else if (el.hasAttribute(ATTR_PROXIFIED_SITE)) {
- if (el[destinationAttr] !== el.getAttribute(ATTR_PROXIFIED_SITE))
- el[destinationAttr] = el.getAttribute(ATTR_PROXIFIED_SITE);
- el.classList.add(CLASS_PROXIFIED_LIVE);
- }
-
- // Optional: Re-render element to encourage browsers to reflect proxified link
- // TODO: Consider aria-live for screenreaders
- if (ENABLE_RERENDER) {
- const isElSelected = document.activeElement == el || el.contains(document.activeElement);
- const elStyleDisplay = el.style.display;
-
- // hide the element and replace with a placeholder clone to prevent 1-frame flash
- const CLONE_CLASSNAME = 'proxi-tied';
- const elClone = el.cloneNode(true); // deep clone to preserve DOM, at cost of perf
- elClone.classList.add(CLONE_CLASSNAME);
- elClone.style = 'color: black !important; background: black !important;';
- if (!el.parentNode?.querySelector(`.${CLONE_CLASSNAME}`) && elStyleDisplay !== 'none') {
- el.parentNode?.insertBefore(elClone, el.nextSibling);
-
- if (isElSelected) el.blur();
- el.style.display = 'none';
-
- setTimeout(() => {
- // show and select the original element
- el.style.display = elStyleDisplay;
- if (isElSelected) el.focus();
-
- if (elClone.parentNode === el.parentNode) el.parentNode?.removeChild(elClone);
- }, 0);
- }
- }
- }
-
- /**
- * Rerender state to all proxified anchor elements in the document
- *
- * @param {boolean} doHoveredOnly Only render proxified elements that are being hovered
- * @returns {void}
- */
- function updateAllProxifiedAnchors(doHoveredOnly) {
- const proxifiedEls = document.querySelectorAll(
- `a.${CLASS_PROXIFIED}${doHoveredOnly ? ':is(:hover, :focus-within)' : ''}`
- );
- for (const proxifiedEl of proxifiedEls) {
- updateProxifiedElement(proxifiedEl, 'href');
- }
- }
-
- /**
- * Add proxified link styles to indicate functionality and readiness
- *
- * Highlight proxied links yellow
- * Highlight Farside-redirected links green
- */
- // TODO: Manually apply inline element styles to avoid style-src CSP
- const isHoverOnly = true; // toggle styling on when hovering on link
- const _important = true ? '!important' : ''; // toggle overriding page styles as much as possible
- // Style anchor element and children under anchor element
- const selectorAnchor = isHoverOnly
- ? `a.${CLASS_PROXIFIED_LIVE}:is(:hover, :focus-within)`
- : `a.${CLASS_PROXIFIED_LIVE}`;
- // add additional styles at higher specificity to child containers with non-empty content
- const selectorChildren = ':is(p, span, h1, h2, h3, h4, h5, h6, label, div, button):not(:empty)';
- addPageStyle(
- `${selectorAnchor} { \
- color: black ${_important}; \
- background-color: yellow ${_important}; \
- text-shadow: none ${_important}; \
- \
- font-style: oblique ${_important}; \
- font-weight: bold ${_important}; \
- \
- ${selectorChildren} { \
- color: yellow ${_important}; \
- background-color: black ${_important}; \
- text-shadow: none ${_important}; \
- \
- font-style: oblique ${_important}; \
- font-weight: bold ${_important}; \
- } \
- :is(img) { \
- filter: sepia(1) hue-rotate(20deg) contrast(1.25) brightness(1.25); ${_important}; \
- }\
- }`
- );
- // Add optional highlighter padding at lowest specificity :where()
- addPageStyle(
- `:where(${selectorAnchor}) { \
- // padding: 0 0.3em; \
- \
- ${selectorChildren} { \
- padding: 0 0.3em; \
- } \
- }`
- );
- // Change colors when links redirect through Farside
- addPageStyle(
- `${selectorAnchor}[${ATTR_IS_FARSIDE}="true"] { \
- color: yellowgreen ${_important}; \
- background-color: black ${_important}; \
- \
- ${selectorChildren} { \
- color: black ${_important}; \
- background-color: yellowgreen ${_important}; \
- } \
- :is(img) { \
- filter: invert(1) sepia(1) hue-rotate(45deg) contrast(1.25) brightness(1.25) ${_important}; \
- } \
- }`
- );
- // Highlight all iframes when frame bite is enabled
- addPageStyle(
- `body.proxi-bite.proxi-framebite { \
- iframe { \
- border: yellow solid 0.3em ${_important}; \
- } \
- iframe[${ATTR_IS_FARSIDE}="true"] { \
- border-color: yellowgreen ${_important}; \
- } \
- }`
- );
-
- /**
- * Insert a page-level style
- * Adds a document stylesheet to write to when needed
- *
- * @param {string} css
- * @returns {void}
- */
- function addPageStyle(css) {
- const style =
- document.getElementById(STYLE_HIGHLIGHT_ID) ||
- (function () {
- const style = document.createElement('style');
- style.id = STYLE_HIGHLIGHT_ID;
- document.head.appendChild(style);
- return style;
- })();
-
- const sheet = style.sheet;
- try {
- sheet.insertRule(css, (sheet.rules || sheet.cssRules || []).length);
- } catch (ex) {
- // Likely stylesheet is null from failing to add to DOM
- // Continue on, even without styling
- warn(`Failed to apply style: ${ex}`);
- }
- }
-
- /**
- * Check given link url and text against provided exclusion rule
- *
- * @param {URL} url
- * @param {string} text
- * @param {Exclusion} rule
- * @returns {string?} First found exclusion reason, null if false
- */
- function isLinkExcludedByRule(url, text, rule) {
- if (!rule) {
- error(`Invalid flattened exclusion rule for ${url}`);
- return 'Invalid flatted exclusion rule';
- }
-
- // Return reason if excluding the current page that this link is on
- // Check first to short circuit link processing on pages proxifying is excluded
- // Results are memoized by page after being processed once, manually invalidate `_excludedOn` if needed
- const currentDirection = `${window.location.href}=>${url.host}`;
- if (!!_excludedOn[currentDirection]) {
- return _excludedOn[currentDirection];
- }
- if (!!rule.excludeOn && _excludedOn[currentDirection] === undefined) {
- /** @type {URL | null} */
- let currentUrl = null;
- try {
- currentUrl = new URL(currentDirection);
- } catch (ex) {
- // unexpected failure on parsing current URL, error and continue
- error(`Error parsing page URL: ${ex}`);
- }
-
- if (!!rule.excludeOn.matchingPath) {
- for (const m of rule.excludeOn.matchingPath) {
- if (!!currentUrl.pathname.match(m)) {
- _excludedOn[currentDirection] = `excludeOn.matchingPath[${m}]`;
- }
- }
- }
-
- if (!!rule.excludeOn.matchingHost) {
- for (const m of rule.excludeOn.matchingHost) {
- if (!!currentUrl.host.match(m)) {
- _excludedOn[currentDirection] = `excludeOn.matchingHost[${m}]`;
- }
- }
- }
-
- if (!!rule.excludeOn.matchingUrl) {
- for (const m of rule.excludeOn.matchingUrl) {
- if (!!currentUrl.href.match(m)) {
- _excludedOn[currentDirection] = `excludeOn.matchingUrl[${m}]`;
- }
- }
- }
-
- if (!!rule.excludeOn.matchingBody) {
- for (const m of rule.excludeOn.matchingBody) {
- if (document.getElementsByTagName('body')[0].innerHTML.match(m)) {
- _excludedOn[currentDirection] = `excludeOn.matchingBody[${m}]`;
- }
- }
- }
-
- if (!!rule.excludeOn.matchingHead) {
- // clean before searching
- // if the userscript extension mounts to head, may false positive on the userscript itself
- if (_documentHeadCleaned === undefined) {
- const documentHeadTemp = document.createElement('head');
- documentHeadTemp.innerHTML = document.getElementsByTagName('head')[0]?.innerHTML;
- let s;
- for (const scriptNodes = documentHeadTemp.getElementsByTagName('script'); (s = scriptNodes[0]); ) {
- // cut down scriptNodes queue
- s.parentNode.removeChild(s);
- }
- _documentHeadCleaned = documentHeadTemp.innerHTML;
- }
-
- for (const m of rule.excludeOn.matchingHead) {
- if (_documentHeadCleaned.match(m)) {
- _excludedOn[currentDirection] = `excludeOn.matchingHead[${m}]`;
- }
- }
- }
-
- // process this page url once even if no matches found for this page
- // return any non-nullish match found, or continue to find other link exclusions
- if (_excludedOn[currentDirection] === undefined) {
- _excludedOn[currentDirection] = null;
- } else if (!!_excludedOn[currentDirection]) {
- return _excludedOn[currentDirection];
- }
- }
-
- // Return reason if excluding this link
- if (!!rule.excludeLinks) {
- if (!!rule.excludeLinks.matchingPath) {
- for (const m of rule.excludeLinks.matchingPath) {
- if (!!url.pathname.match(m)) return `excludeLinks.matchingPath[${m}]`;
- }
- }
-
- if (!!rule.excludeLinks.matchingHost) {
- for (const m of rule.excludeLinks.matchingHost) {
- if (!!url.host.match(m)) return `excludeLinks.matchingHost[${m}]`;
- }
- }
-
- if (!!rule.excludeLinks.matchingUrl) {
- for (const m of rule.excludeLinks.matchingUrl) {
- if (!!url.href.match(m)) return `excludeLinks.matchingUrl[${m}]`;
- }
- }
-
- if (!!rule.excludeLinks.matchingText) {
- for (const m of rule.excludeLinks.matchingText) {
- if (!!text.match(m)) return `excludeLinks.matchingText[${m}]`;
- }
- }
- }
- }
- const _excludedOn = {}; // memoized excludeOn results
- let _documentHeadCleaned; // memoized cleaned <head>
-
- /**
- * Check if element has been fully processed already
- * @param {HTMLElement} el
- * @returns {boolean}
- */
- function isElementProxified(el) {
- // bonafide attr signals completed proxification
- // successful: all proxified attrs set
- // denied: denied + bonafide attrs set
- const elBonafide = el?.getAttribute(ATTR_BONAFIDE_SITE);
-
- // proxification is outdated if current link does not match any stored destination
- // this can occur when the same link el is reused dynamically by the site scripts and must be invalidated
- const linkDest = getElementDest(el);
- const isUpToDate =
- linkDest === elBonafide ||
- linkDest === el?.getAttribute(ATTR_PROXIFIED_SITE) ||
- linkDest === el?.getAttribute(ATTR_PROXIFIED_FARSIDE);
-
- return elBonafide && isUpToDate;
- }
-
- /**
- * Get the link destination
- * @param {HTMLElement} el
- * @returns {string?}
- */
- function getElementDest(el) {
- switch (el.constructor) {
- case HTMLAnchorElement:
- return el.href;
- case HTMLIFrameElement:
- return el.src;
- default:
- return null;
- }
- }
-
- /**
- * Returns the dictionary entry for the specified host, agnostic to WWW
- * Take explicit host match if found, but optionally try searching for SLD+TLD only (strip subdomains)
- *
- * @param {Object<string, T?>} dictionary
- * @param {string} host
- * @param {boolean} allowFuzzy
- * @returns {T?}
- */
- function getByHost(dictionary, host, allowFuzzy) {
- if (typeof dictionary !== 'object') {
- error('Invalid dictionary used to look up host');
- return null;
- }
-
- // Find the explicit entry for the host key
- const explicit = dictionary[host];
- if (!!explicit) {
- return explicit;
- }
-
- // Strip WWW and try again
- const agnosticWWW = dictionary[host.replace(/^www\./, '')];
- if (!!agnosticWWW) {
- return agnosticWWW;
- }
-
- // Non-WWW subdomains in hostname may be causing misses
- // Too many TLDs to handle easily, so allow fuzzy matching on SLD+TLD if enabled
- // Search for dictionary keys matching to the end of host and try the first hit
- if (allowFuzzy) {
- // try any dictionary key that has opted into fuzzy matching
- const candidateHostKey = Object.keys(dictionary)
- .filter(hostKey => !!dictionary[hostKey].allowFuzzy)
- .find(hostKey => {
- // try to fit the key to the end of the matching host
- const SLDTLDRegex = new RegExp(escapeRegExp(hostKey) + '$');
- return SLDTLDRegex.test(host);
- });
-
- const agnosticSubdomain = dictionary[candidateHostKey];
- if (!!agnosticSubdomain) {
- return agnosticSubdomain;
- }
- }
-
- return null;
- }
-
- /**
- * Escape regex special characters in a string
- * TC39 X-standard
- *
- * @param {string} string
- * @returns {string}
- */
- function escapeRegExp(string) {
- return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
- }
-
- /**
- * Print to console as error
- *
- * @param {...any} data
- */
- function error(...data) {
- if (DEBUG) {
- console.error(...data);
- }
- }
-
- /**
- * Print to console as warning
- *
- * @param {...any} data
- */
- function warn(...data) {
- if (DEBUG) {
- console.warn(...data);
- }
- }