const LANGS = ["DE", "EN", "ES", "FR", "IT", "KO", "PL", "PT", "TOK", "UK", "ZH"]; const currentURL = new URL(document.location); let filteredWikis = []; let hiddenWikisRevealed = {}; // Create object prototypes for getting and setting attributes: Object.prototype.get = function (prop) { this[prop] = this[prop] || {}; return this[prop]; }; Object.prototype.set = function (prop, value) { this[prop] = value; } // Function to create an observer to watch for mutations on search pages // This is used for search engines that paginate via JavaScript, // or overwrite their results and remove IWB's elements function addLocationObserver(callback) { const config = { attributes: false, childList: true, subtree: true } const observer = new MutationObserver(callback); observer.observe(document.body, config); } // Load website data: async function getData() { let sites = []; let promises = []; for (let i = 0; i < LANGS.length; i++) { promises.push(fetch(chrome.runtime.getURL('data/sites' + LANGS[i] + '.json')) .then((resp) => resp.json()) .then(function (jsonData) { jsonData.forEach((site) => { site.origins.forEach((origin) => { sites.push({ "id": site.id, "origin": origin.origin, "origin_group": site.origins_label, "origin_base_url": origin.origin_base_url, "origin_content_path": origin.origin_content_path, "destination": site.destination, "destination_base_url": site.destination_base_url, "destination_content_path": site.destination_content_path, "destination_content_prefix": (site.destination_content_prefix ? site.destination_content_prefix : ""), "destination_platform": site.destination_platform, "destination_icon": site.destination_icon, "lang": LANGS[i] }) }) }); })); } await Promise.all(promises); return sites; } function insertCSS() { // Output CSS styleString = ` .iwb-notice { display: block !important; margin: .5em .5em 1em .5em !important; padding: .5em .5em .5em 1em !important; border-left: 3px solid #FFCC33 !important; font-size: 14px !important; color: white !important; mix-blend-mode: difference !important; } .iwb-notice a { text-decoration: underline !important; color: white !important; mix-blend-mode: difference !important; } .iwb-notice button { cursor: pointer !important; display: inline-block !important; padding: 2px 8px !important; margin: 8px 8px 0 0 !important; background-color: transparent !important; border: 1px solid !important; border-radius: 5px !important; font-size: 12px !important; color: white !important; mix-blend-mode: difference !important; text-align: left !important; } .iwb-hide { display: none !important; } .iwb-show { display: block !important; } .iwb-notice button:hover { outline: 1px solid !important; } .iwb-new-link-container { display: inline-block; font-size: 12px !important; text-decoration: none; padding-left: 5px; position: relative; } .iwb-new-link { display: inline-block; text-decoration: none; position: relative; } .iwb-new-link:hover { text-decoration: none; z-index: 9999999; } .iwb-new-link button { cursor: pointer; color: white; background: #005799; border: 0px solid #fff; border-radius: 10px; padding: 5px 10px; margin: .5em 0; font-size: 1.2em; width: fit-content; } .iwb-new-link button:hover { background: #00467a; } .iwb-new-link button * { vertical-align: middle; } .iwb-new-link div:first-of-type { display: inline-block; background: white; border-radius: 16px; margin-right: 10px; width: fit-content; height: fit-content; line-height: 12px; padding: 5px; } .iwb-new-link img { width: 12px; } .iwb-disavow > *:not([class^="iwb-"]) { opacity: 60%; pointer-events: none; cursor: default; } .iwb-disavow > a:not([class^="iwb-"]), .iwb-disavow > *:not([class^="iwb-"]) a, .iwb-disavow > *:not([class^="iwb-"]) a * { text-decoration: line-through !important; } .iwb-result-controls { display: inline-block; margin: .5em; font-size: 12px; color: white !important; mix-blend-mode: difference !important; } .iwb-result-controls > div { display: inline-block; cursor: pointer; text-decoration: underline; text-decoration-style: dotted; text-decoration-thickness: 1px; padding: 0 1em; } .iwb-notice .iwb-result-controls { margin: 8px 0 0 0; } ` style = document.createElement('style'); style.classList.add('iwb-styles'); style.textContent = styleString; document.head.append(style); } // Function to convert strings to consistent IDs // Used to convert wiki names to element IDs function stringToId(string) { return string.replaceAll(' ', '-').replaceAll("'", '').replace(/\W/g, '').toLowerCase(); } // Function to escape string to use in regex function escapeRegex(string) { return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } function replaceSearchResults(searchResultContainer, site, link) { // Build new URL: let article = link.split(site['origin_content_path'])[1]?.split('#')[0].split('?')[0].split('&')[0]; let newURL = ''; if (article) { let searchParams = ''; switch (site['destination_platform']) { case 'mediawiki': searchParams = 'Special:Search/' + site['destination_content_prefix'] + article; break; case 'doku': searchParams = 'start?do=search&q=' + article; break; } newURL = 'https://' + site['destination_base_url'] + site['destination_content_path'] + searchParams; } else { newURL = 'https://' + site['destination_base_url']; } if (searchResultContainer && !searchResultContainer.querySelector('.iwb-new-link')) { if (!searchResultContainer.classList.contains('iwb-detected')) { searchResultContainer.classList.add('iwb-detected'); searchResultContainer.classList.add('iwb-disavow'); } // Using aside to avoid conflicts with website CSS and listeners: let indieContainer = document.createElement('aside'); indieContainer.classList.add('iwb-new-link-container'); let indieResultLink = document.createElement('a'); indieResultLink.href = newURL; indieResultLink.classList.add('iwb-new-link'); let indieResultButton = document.createElement('button'); let indieResultFaviconContainer = document.createElement('div'); let indieResultFavicon = document.createElement('img'); indieResultFavicon.alt = ''; indieResultFavicon.width = '12'; indieResultFavicon.height = '12'; indieResultFavicon.src = chrome.runtime.getURL('favicons/' + site.lang.toLowerCase() + '/' + site.destination_icon); indieResultFaviconContainer.append(indieResultFavicon); let indieResultText = document.createElement('span'); if (article) { if (site['lang'] === 'EN' && link.match(/fandom\.com\/[a-z]{2}\/wiki\//)) { indieResultText.innerText = 'Look up "' + decodeURIComponent(decodeURIComponent(article.replaceAll('_', ' '))) + '" on ' + site.destination + ' (EN)'; } else { indieResultText.innerText = 'Look up "' + decodeURIComponent(decodeURIComponent(article.replaceAll('_', ' '))) + '" on ' + site.destination; } } else { if (site['lang'] === 'EN' && link.match(/fandom\.com\/[a-z]{2}\/wiki\//)) { indieResultText.innerText = 'Visit ' + site.destination + ' (EN) instead'; } else { indieResultText.innerText = 'Visit ' + site.destination + ' instead'; } } indieResultButton.append(indieResultFaviconContainer); indieResultButton.append(indieResultText); indieResultLink.appendChild(indieResultButton); // Output container for result controls: let resultControls = document.createElement('div'); resultControls.classList.add('iwb-result-controls'); // Output link to re-enable disabled result: let enableResultButton = document.createElement('div'); enableResultButton.innerText = 'Re-enable the result below'; resultControls.prepend(enableResultButton); enableResultButton.addEventListener('click', function (e) { e.target.closest('.iwb-disavow').classList.remove('iwb-disavow'); e.target.classList.add('iwb-hide'); }); indieContainer.appendChild(indieResultLink); indieContainer.appendChild(resultControls); searchResultContainer.prepend(indieContainer); return 1; } return 0; } function hideSearchResults(searchResultContainer, searchEngine, site) { // Insert search result removal notice if (!filteredWikis.includes(site.lang + ' ' + site.origin_group)) { filteredWikis.push(site.lang + ' ' + site.origin_group); let elementId = stringToId(site.lang + '-' + site.origin_group); hiddenWikisRevealed[elementId] = false; // Using aside to avoid conflicts with website CSS and listeners: let searchRemovalNotice = document.createElement('aside'); searchRemovalNotice.id = 'iwb-notice-' + elementId; searchRemovalNotice.classList.add('iwb-notice'); let searchRemovalNoticeLink = document.createElement('a'); searchRemovalNoticeLink.href = 'https://' + site.destination_base_url; searchRemovalNoticeLink.textContent = site.destination; searchRemovalNoticePretext = document.createTextNode('Indie Wiki Buddy has filtered out results from ' + site.origin_group + (site.lang !== 'EN' ? ' (' + site.lang + ')' : '') + '. Look for results from '); searchRemovalNoticePosttext = document.createTextNode(' instead.'); searchRemovalNotice.appendChild(searchRemovalNoticePretext); searchRemovalNotice.appendChild(searchRemovalNoticeLink); searchRemovalNotice.appendChild(searchRemovalNoticePosttext); // Output container for result controls: let resultControls = document.createElement('div'); resultControls.classList.add('iwb-result-controls'); // Output link to show hidden results: let showResultsButton = document.createElement('div'); showResultsButton.setAttribute('data-group', 'iwb-search-result-' + elementId); showResultsButton.innerText = 'Show filtered results'; resultControls.appendChild(showResultsButton) showResultsButton.onclick = function (e) { if (e.target.textContent.includes('Show')) { e.target.textContent = 'Re-hide filtered results'; hiddenWikisRevealed[elementId] = true; const selector = e.currentTarget.dataset.group; document.querySelectorAll('.' + selector).forEach(el => { el.classList.add('iwb-show'); }) } else { e.target.textContent = 'Show filtered results'; hiddenWikisRevealed[elementId] = false; const selector = e.currentTarget.dataset.group; document.querySelectorAll('.' + selector).forEach(el => { el.classList.remove('iwb-show'); }) } } searchRemovalNotice.appendChild(resultControls); switch (searchEngine) { case 'google': if (document.querySelector('#search')) { document.querySelector('#search').prepend(searchRemovalNotice); } else if (document.querySelector('#topstuff')) { document.querySelector('#topstuff').prepend(searchRemovalNotice); } else if (document.querySelector('#main')) { var el = document.querySelector('#main'); if (el.querySelector('#main > div[data-hveid]')) { el.insertBefore(searchRemovalNotice, el.querySelector('div[data-hveid]')); } else { el.insertBefore(searchRemovalNotice, el.querySelector('div div[data-hveid]').parentElement); } }; break; case 'bing': var li = document.createElement('li'); li.appendChild(searchRemovalNotice); document.querySelector('#b_results').prepend(li); break; case 'duckduckgo': if (document.getElementById('web_content_wrapper')) { var li = document.createElement('li'); li.appendChild(searchRemovalNotice); document.querySelector('#web_content_wrapper ol').prepend(li); } else { document.getElementById('links').prepend(searchRemovalNotice); } break; case 'brave': document.querySelector('body').prepend(searchRemovalNotice); break; case 'ecosia': document.querySelector('body').prepend(searchRemovalNotice); break; case 'startpage': document.querySelector('#main').prepend(searchRemovalNotice); break; case 'yahoo': if (document.querySelector('#web > ol')) { var li = document.createElement('li'); li.appendChild(searchRemovalNotice); document.querySelector('#web > ol').prepend(li); } else { document.querySelector('#main-algo').prepend(searchRemovalNotice); } break; default: } } if (!Array.from(searchResultContainer.classList).includes('iwb-hide')) { let elementId = stringToId(site.lang + '-' + site.origin_group); searchResultContainer.classList.add('iwb-search-result-' + elementId); searchResultContainer.classList.add('iwb-hide'); searchResultContainer.classList.add('iwb-detected'); if (hiddenWikisRevealed[elementId]) { searchResultContainer.classList.add('iwb-show'); } return 1; } return 0; } function filterSearchResults(searchResults, searchEngine, storage) { getData().then(sites => { let crossLanguageSetting = storage.crossLanguage || 'off'; let countFiltered = 0; for (let searchResult of searchResults) { try { // Check that result isn't within another result if (!searchResult.closest('.iwb-detected')) { let searchResultLink = ''; if (searchEngine === 'bing') { searchResultLink = searchResult.innerHTML.replaceAll('', '').replaceAll('', ''); } else { searchResultLink = searchResult.closest('a[href]').href; } let link = String(decodeURIComponent(searchResultLink)); if (searchEngine === 'google') { // Break if image result: if (link.includes('imgurl=')) { break; } } // Check if site is in our list of wikis: if (crossLanguageSetting === 'on') { matchingSites = sites.filter(el => link.replace(/.*https?:\/\//, '').startsWith(el.origin_base_url)); } else { matchingSites = sites.filter(el => link.replace(/.*https?:\/\//, '').startsWith(el.origin_base_url + el.origin_content_path)); } if (matchingSites.length > 0) { // Select match with longest base URL let closestMatch = ""; matchingSites.forEach(site => { if (site.origin_base_url.length > closestMatch.length) { closestMatch = site.origin_base_url; } }); let site = matchingSites.find(site => site.origin_base_url === closestMatch); if (site) { // Get user's settings for the wiki let id = site['id']; let searchFilterSetting = 'replace'; if (storage.searchEngineSettings && storage.searchEngineSettings[id]) { searchFilterSetting = storage.searchEngineSettings[id]; } else if (storage.defaultSearchAction) { searchFilterSetting = storage.defaultSearchAction; } if (searchFilterSetting !== 'disabled') { // Output stylesheet if not already done if (filteredWikis.length === 0) { // Wait for head to be available const headElement = document.querySelector('head'); if (headElement && !document.querySelector('.iwb-styles')) { insertCSS(); } else { const docObserver = new MutationObserver(function (mutations, mutationInstance) { const headElement = document.querySelector('head'); if (headElement && !document.querySelector('.iwb-styles')) { insertCSS(); mutationInstance.disconnect(); } }); docObserver.observe(document, { childList: true, subtree: true }); } } let searchResultContainer = null; switch (searchEngine) { case 'google': searchResultContainer = searchResult.closest('div[data-hveid]'); break; case 'bing': searchResultContainer = searchResult.closest('li.b_algo'); break; case 'duckduckgo': searchResultContainer = searchResult.closest('li[data-layout], div.web-result'); break; case 'brave': searchResultContainer = searchResult.closest('div.snippet'); break; case 'ecosia': searchResultContainer = searchResult.closest('div.mainline__result-wrapper article div.result__body'); break; case 'startpage': searchResultContainer = searchResult.closest('div.w-gl__result'); break; case 'yahoo': searchResultContainer = searchResult.closest('#web > ol > li div.itm .exp, #web > ol > li div.algo, #web > ol > li, section.algo'); break; default: } if (searchResultContainer) { if (searchFilterSetting === 'hide') { countFiltered += hideSearchResults(searchResultContainer, searchEngine, site); } else { countFiltered += replaceSearchResults(searchResultContainer, site, link); } } } } } } } catch (e) { console.log('Indie Wiki Buddy failed to properly parse search results with error: ' + e); } }; addLocationObserver(main); if (countFiltered > 0) { chrome.storage.sync.set({ 'countSearchFilters': (storage.countSearchFilters ?? 0) + countFiltered }); } }); } function main(mutations = null, observer = null) { if (observer) { observer.disconnect(); } chrome.storage.local.get(function (localStorage) { chrome.storage.sync.get(function (syncStorage) { const storage = { ...syncStorage, ...localStorage }; // Check if extension is on: if ((storage.power ?? 'on') === 'on') { // Determine which search engine we're on if (currentURL.hostname.includes('www.google.')) { // Function to filter search results in Google function filterGoogle() { let searchResults = document.querySelectorAll("div[data-hveid] a[href*='fandom.com']:first-of-type:not([role='button']):not([target]), div[data-hveid] a[href*='fextralife.com']:first-of-type:not([role='button']):not([target])"); filterSearchResults(searchResults, 'google', storage); } // Wait for document to be interactive/complete: if (['interactive', 'complete'].includes(document.readyState)) { filterGoogle(); } else { document.addEventListener('readystatechange', e => { if (['interactive', 'complete'].includes(document.readyState)) { filterGoogle(); } }, { once: true }); } } else if (currentURL.hostname.includes('duckduckgo.com') && (currentURL.search.includes('q=') || currentURL.pathname.includes('html'))) { // Function to filter search results in DuckDuckGo function filterDuckDuckGo() { let searchResults = document.querySelectorAll('h2>a[href*="fandom.com"], h2>a[href*="fextralife.com"]'); filterSearchResults(searchResults, 'duckduckgo', storage); } // Wait for document to be interactive/complete: if (['interactive', 'complete'].includes(document.readyState)) { filterDuckDuckGo(); } else { document.addEventListener('readystatechange', e => { if (['interactive', 'complete'].includes(document.readyState)) { filterDuckDuckGo(); } }, { once: true }); } } else if (currentURL.hostname.includes('www.bing.com')) { // Function to filter search results in Bing function filterBing() { let searchResults = Array.from(document.querySelectorAll('.b_attribution>cite')).filter(el => el.innerHTML.replaceAll('', '').replaceAll('', '').includes('fandom.com') || el.innerHTML.replaceAll('', '').replaceAll('', '').includes('fextralife.com') ); filterSearchResults(searchResults, 'bing', storage); } // Wait for document to be interactive/complete: if (['interactive', 'complete'].includes(document.readyState)) { filterBing(); } else { document.addEventListener('readystatechange', e => { if (['interactive', 'complete'].includes(document.readyState)) { filterBing(); } }, { once: true }); } } else if (currentURL.hostname.includes('search.brave.com')) { // Function to filter search results in Brave function filterBrave() { let searchResults = Array.from(document.querySelectorAll('div.snippet[data-type="web"] a')).filter(el => el.innerHTML.includes('fandom.com') || el.innerHTML.includes('fextralife.com')); filterSearchResults(searchResults, 'brave', storage); } // Wait for document to be interactive/complete: if (['interactive', 'complete'].includes(document.readyState)) { filterBrave(); } else { document.addEventListener('readystatechange', e => { if (['interactive', 'complete'].includes(document.readyState)) { filterBrave(); } }, { once: true }); } } else if (currentURL.hostname.includes('ecosia.org')) { // Function to filter search results in Ecosia function filterEcosia() { let searchResults = Array.from(document.querySelectorAll('section.mainline .result__title a.result__link')).filter(el => el.href.includes('fandom.com') || el.href.includes('fextralife.com')); filterSearchResults(searchResults, 'ecosia', storage); } // Wait for document to be interactive/complete: if (['interactive', 'complete'].includes(document.readyState)) { filterEcosia(); } else { document.addEventListener('readystatechange', e => { if (['interactive', 'complete'].includes(document.readyState)) { filterEcosia(); } }, { once: true }); } } else if (currentURL.hostname.includes('startpage.com')) { // Function to filter search results in Startpage function filterStartpage() { let searchResults = Array.from(document.querySelectorAll('a.result-link')).filter(el => el.href.includes('fandom.com') || el.href.includes('fextralife.com')); filterSearchResults(searchResults, 'startpage', storage); } // Wait for document to be interactive/complete: if (['interactive', 'complete'].includes(document.readyState)) { filterStartpage(); } else { document.addEventListener('readystatechange', e => { if (['interactive', 'complete'].includes(document.readyState)) { filterStartpage(); } }, { once: true }); } } else if (currentURL.hostname.includes('yahoo.com')) { // Function to filter search results in Yahoo function filterYahoo() { let searchResults = Array.from(document.querySelectorAll('#web > ol > li a:not(.thmb), #main-algo section.algo a:not(.thmb)')).filter(el => el.href.includes('fandom.com') || el.href.includes('fextralife.com')); filterSearchResults(searchResults, 'yahoo', storage); } // Wait for document to be interactive/complete: if (['interactive', 'complete'].includes(document.readyState)) { filterYahoo(); } else { document.addEventListener('readystatechange', e => { if (['interactive', 'complete'].includes(document.readyState)) { filterYahoo(); } }, { once: true }); } } } }); }); } main();