FaviconizeGoogle

Adds favicons next to Google search results. Also works for Ecosia.

Verze ze dne 15. 06. 2020. Zobrazit nejnovější verzi.

K instalaci tototo skriptu si budete muset nainstalovat rozšíření jako Tampermonkey, Greasemonkey nebo Violentmonkey.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Violentmonkey.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Violentmonkey.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Userscripts.

You will need to install an extension such as Tampermonkey to install this script.

K instalaci tohoto skriptu si budete muset nainstalovat manažer uživatelských skriptů.

(Už mám manažer uživatelských skriptů, nechte mě ho nainstalovat!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(Už mám manažer uživatelských stylů, nechte mě ho nainstalovat!)

// ==UserScript==
// @name           FaviconizeGoogle
// @namespace      http://userscripts.org/users/89794   (joeytwiddle)
// @description    Adds favicons next to Google search results.  Also works for Ecosia.
// @homepage       https://greatest.deepsurf.us/en/scripts/7664-faviconizegoogle
// @downstreamURL  http://userscripts.org/scripts/source/48636.user.js
// @license        ISC
// @version        1.6.0
// @include        /https?:\/\/((www\.)?|encrypted\.)google\.[a-z]{2,3}(\.[a-z]{2})?\/(search|webhp|\?gws_rd|\?gfe_rd)?.*/
// @include        /https?:\/\/(www\.|[a-z0-9-]*\.)?startpage\.com\/.*/
// @include        /https?:\/\/(www\.)?ecosia\.org\/(search|news|videos)?.*/
// @grant          GM_xmlhttpRequest
// @connect        *
// ==/UserScript==

var placeFaviconByUrl      = false;   // The little green link below the article title
var placeFaviconAfter      = false;   // Display after the link instead of before it
var placeFaviconInsideLink = false;   // Makes the favicon clickable but may also get underlined
var placeFaviconOffTheLeft = true;    // Makes the favicon sit out to the left of the main column (not on startpage)
var iconSize               = 1.3;
var centraliseIconVertically = iconSize < 2;   // For smaller icon sizes, we center-align with the text, for larger we top-align

// With thanks to:
// - NV
// - darkred
// - SirCumference

// Some alternatives/remixes:
// - https://github.com/NV/faviconize-google.js (Chrome extension)
// - https://greatest.deepsurf.us/en/scripts/12395-google-favicons (works with Endless Google)
// - https://gist.github.com/Sir-Cumference/223d36cbec6473b0e6927e5c50c11568 (very short code, @match works with Greasemonkey)

// 2020-04-21 Re-enabled for Ecosia and StartPage, bypass CSP by fetching the images using GM_xmlhttpRequest.
// 2020-02-02 Reenabled Google again, because Google has stopped showing favicons.
// 2020-01-22 Disabled Google, because Google is now displaying favicons itself.
// 2020-01-22 Disabled Ecosia, because their CSP is blocking images
// 2018-10-14 Added support for ecosia.org!
// 2018-10-14 Disabled startpage.com, because their CSP has blocked favicons from loading.
// 2018-07-31 Dropped support for news.google.com, because it is now linking to local URLs, instead of to external websites.

// TODO: The relative positioning of the icon appears a bit off for sub-links of the main result.

// DONE: Provided more options where to place favicon: by the link or by the
// url, before or after, inside or outside the link.  However in my opinion
// they all suck except the default. ;)

// Broken images would be messy, but Firefox seems to hide them after a while
// anyway.  We do still see the gap from the image's padding though!
// It might be desirable to check each image actually exists/loads, or remove it.
// Is that possible, without making an http request ourselves?

// Third-party host URL detection is implemented leniently, and accordingly
// hostname extraction implemented aggressively, which results in favicons
// being given to unexpected things like bookmarklets which contain a site url.

var isEcosia = document.location.hostname === 'www.ecosia.org' || document.location.hostname === 'ecosia.org';
var isStartpage = document.location.host.match(/\bstartpage\b/);

//var bypassCSP = isEcosia || isStartpage;
// Enable it for all sites.  I think it's more secure for us to fetch the icon by GM_xhr rather than injecting it into the page.
var bypassCSP = true;

if (isEcosia) {
	iconSize = iconSize * 0.6;
}

if (isStartpage) {
	// This feature doesn't work on startpage!
	placeFaviconOffTheLeft = false;
}

if (document.location.search.match(/&tbm=nws/)) {
	// This feature doesn't work on Google News search
	placeFaviconOffTheLeft = false;
	// The layout is a bit too cramped for large favicons
	iconSize = 0.9;
}

if ((isEcosia || isStartpage) && (window.innerWidth < 1000)) {
	// At lower widths, these website collapse their layout so there is no margin on the left, in which case we don't want to float the favicon out of view.
	// We dare not do this for Google, as this isn't working right now!
	placeFaviconOffTheLeft = false;
}

function findClosest (elem, tagName) {
	// eslint-disable-next-line no-cond-assign
	while (elem = elem.parentNode) {
		if (elem.tagName && elem.tagName.toLowerCase() === tagName.toLowerCase()) {
			return elem;
		}
	}
	return null;
}

function createFaviconFor (url) {
	var host = url.replace(/^[^/]*:\/\//, '').replace(/\/.*$/, '');
	// if (host == document.location.host) {
		// return null;
	// }
	// Use protocol (http/https) of current page, to avoid mixed-content warnings/failures.
	//var protocol = document.location.protocol.replace(/:$/, '');
	//console.log("[FaviconizeGoogle.user.js] protocol:" ,protocol);
	// We may fail to access favicons, e.g. if the site only offers http, or due to CORS restrictions.
	// But it is still worth tryiing, because the fallback (google's s2) offers only low-res icons.
	var urlsToTry = [
		'//' + host + '/favicon.ico',
		'//' + host + '/favicon.png',
		'//www.google.com/s2/favicons?domain=' + host,
	];
	// Google's cache will sometimes provide a favicon we would have missed, e.g. if the site uses .png instead of .ico.  Thanks to NV for suggesting this, and to Google.
	var img = document.createElement('IMG');
	//img.src = protocol + '://'+host+'/favicon.ico';
	//img.src = '//g.etfv.co/http://" + host; // As suggested by decembre
	//img.src = 'http://api.byi.pw/favicon?url=' + protocol + "://" + host; // Another service suggested by decembre
	//img.width = '16';
	//img.height = '16';
	img.className = 'favicon';
	img.border = 0;
	img.style.display = 'none';
	var showImage = function () {
		img.style.display = '';
		if (typeof removeListeners === 'function') {
			removeListeners();
		}
	};
	if (bypassCSP) {
		var blobToDataURL = (blob, callback) => {
			var a = new FileReader();
			a.onload = function(e) {
				callback(e.target.result);
			};
            a.readAsDataURL(blob);
		};
		var tryNextUrlNoCors = () => {
			if (urlsToTry.length === 0) {
				return;
			}
			var url = 'https:' + urlsToTry.shift();
			//console.log('url:', url);
			GM_xmlhttpRequest({
				method: 'GET',
				url: url,
				responseType: 'blob',
				onload: (res) => {
					//console.log('response:', res.response);
					blobToDataURL(res.response, (dataUrl) => {
						//console.log('dataUrl:', dataUrl);
						const isImage = dataUrl.startsWith('data:application/octet-stream;') || dataUrl.startsWith('data:image/');
						if (isImage) {
							img.src = dataUrl;
							showImage();
						} else {
							tryNextUrlNoCors();
						}
					});
				},
			});
		};
		tryNextUrlNoCors();
	} else {
		var tryNextUrl = function () {
			if (urlsToTry.length === 0) {
				removeListeners();
				return;
			}
			img.src = urlsToTry.shift();
		};
		var removeListeners = function () {
			img.removeEventListener('load', showImage);
			img.removeEventListener('error', tryNextUrl);
		};
		img.addEventListener('load', showImage, false);
		img.addEventListener('error', tryNextUrl, false);
		tryNextUrl();
	}
	return img;
}

function getGoogleResultsLinks () {
	// var links = document.evaluate("//a[@class='l']",document,null,6,null);
	// var links = filterListBy(document.links, function(x){ return x.className=='l'; } );
	// var links = document.links.filter( function(x){ return x.className=='l'; } );

	/*
	return filterListBy(document.getElementsByTagName('a'), function (x) {
		// Most pages show links with class 'l'
		// But on pages where the first result has an indented block of sub-pages,
		// the indented links have class 'l' but all the other links have no class but parent class 'r'
		return x.className === 'l' || x.parentNode.className === 'r';
	});
	*/

	// For Google search
	// a.l
	// a.fl are small one-line sub-results.  Search "squeak" and see the Wikipedia result.
	// a.B0IOdd are lists of images which appear underneath a section on the News tab (inside .card-section)
	var links = document.querySelectorAll('.g a:not(.fl):not(.B0IOdd)');

	// For news.google.com
	if (links.length === 0) {
		//links = document.querySelectorAll('a.article:not(.esc-thumbnail-link)');
		links = document.querySelectorAll('article > a');
	}

	// For startpage.com
	if (links.length === 0) {
		//links = document.querySelectorAll('.clk > a');
		links = document.querySelectorAll('.w-gl__result-title');
	}

	// For ecosia.org
	if (links.length === 0) {
		// We use .result-body to avoid getting deep links (which were producing an unwanted second large favicon)
		links = document.querySelectorAll('.result-body .result-title');
	}

	// Remove any links which contain only one image
	links = [...links].filter(link => {
		if (link.childNodes.length === 1 && link.childNodes[0].tagName === 'IMG') {
			return false;
		}
		return true;
	});

	return links;
}

var style = document.createElement('STYLE');
var marginSide = (placeFaviconAfter ? 'left' : 'right');
var leftPadding = 1.2 * iconSize + 0.6 - 0.3 * isEcosia;
// We can try to centralise the icon with the text
// Or we can top-align the icon with the text (better for larger icon sizes)
var topPadding = centraliseIconVertically ? 0.87 - iconSize / 2 : 0.35;
var extra = placeFaviconOffTheLeft ? 'position: absolute; left: -' + leftPadding + 'em; top: ' + topPadding + 'em;' : '';
if (isEcosia) {
	var topMargin = 0.3 + centraliseIconVertically * iconSize / 13;
	// Use margin for vertical positioning
	extra += ' margin-top: ' + (-topMargin) + 'em;';
}
// If we are using placeFaviconOffTheLeft, then we don't need the paddings or alignment here
style.innerHTML = ".favicon { box-sizing: content-box; margin-" + marginSide + ": 0.3em; vertical-align: middle; width: " + iconSize + "em; height: " + iconSize + "em; padding-bottom: 0.2em; " + extra + "}";
document.getElementsByTagName('head')[0].appendChild(style);

function updateFavicons () {
	var links = getGoogleResultsLinks();

	// Allows it to work on any sites:
	if (links.length === 0) {
		links = document.getElementsByTagName("A");
	}
	//console.log("Got links:", links);

	// for (var i=0;i<links.snapshotLength;i++) {
		// var link = links.snapshotItem(i);
	for (var i = 0; i < links.length; i++) {
		var link = links[i];
		// if (link.href.match('^javascript:') || link.href.match('^#')) {
			// continue;
		// }

		var targetUrl = link.getAttribute('data-href') || link.href;

		//// Skip relative and same-host links:
		if (targetUrl.match(/^[/]/) || targetUrl.match("://" + document.location.host)) {
			continue;
		}

		//console.log("[faviconizegoogle.user.js] link.getAttribute(data-faviconized):" ,link.getAttribute("data-faviconized"));
		if (link.getAttribute("data-faviconized")) {
			// Already faviconized
			//console.log("[faviconizegoogle.user.js] Skipping");
			continue;
		}

		// Sometimes Google shows multiple results from one side, side-by-side in a table
		// In this case, adding a favicon to the later links is redundant, and tends to overlap the earlier link
		// So let's not do that
		var tableCell = findClosest(link, 'td');
		if (tableCell && tableCell.parentNode.firstChild !== tableCell) {
			// We are in a table and we are not the first cell; don't add favicon
			continue;
		}

		link.setAttribute("data-faviconized", "yes");
		var img = createFaviconFor(targetUrl);
		// <cite> is for google, .url is for startpage
		var targetNode = (placeFaviconByUrl && link.parentNode.parentNode.querySelector('cite, .url') || link);

		if (isEcosia && !placeFaviconOffTheLeft) {
			// With the default `display: block` the link will break onto a separate line from the favicon, so we do this
			link.style.display = 'inline-block';
			link.parentNode.style.whiteSpace = 'nowrap';
		}

		if (placeFaviconInsideLink) {
			if (placeFaviconAfter) {
				targetNode.appendChild(img);
			} else {
				targetNode.insertBefore(img, targetNode.firstChild);
			}
		} else {
			if (placeFaviconAfter) {
				targetNode.parentNode.insertBefore(img, targetNode.nextSibling);
			} else {
				targetNode.parentNode.insertBefore(img, targetNode);
			}
		}
	}
}

// TODO: Use MutationObserver instead?

var last_srg = null;
var results_count = -1;

function checkForUpdate () {
	// #ires was needed for the News tab, which doesn't have a .srg.  Perhaps we could use ires for all tabs.
	var new_srg = document.querySelector('.srg') || document.querySelector('#ires');
	// startpage
	new_srg = new_srg || document.querySelector('.w-gl--default');
	// ecosia
	new_srg = new_srg || document.querySelector('.mainline');
	//console.log("[FaviconizeGoogle.user.js] last_srg:" ,last_srg);
	//console.log("[faviconizegoogle.user.js] new_srg:" ,new_srg);
	var new_results_count = getGoogleResultsLinks().length;
	if (new_srg !== last_srg || new_results_count !== results_count) {
		//console.log("Page change detected!");
		updateFavicons();
		last_srg = new_srg;
		results_count = new_results_count;
	} else {
		//console.log("Pages are the same:", last_srg, new_srg);
	}
	setTimeout(checkForUpdate, 1000);
}

setTimeout(checkForUpdate, 100);