Retrieve Full Page Titles in Google Search

Fill the page link titles with the full respective page titles, if possible

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name		Retrieve Full Page Titles in Google Search
// @description	Fill the page link titles with the full respective page titles, if possible
// @version		2.0.1
// @namespace	Google
// @author		Benjamin Philipp <dev [at - please don't spam] benjamin-philipp.com>
// @include		/https?:\/\/(www\.)?google\.[a-z\.]{2,6}\/(search|webhp)\?((?!tbm=isch).)*$/
// @require 	http://ajax.googleapis.com/ajax/libs/jquery/3.1.0/jquery.min.js
// @require		https://openuserjs.org/src/libs/sizzle/GM_config.js
// @require 	https://greasyfork.org/scripts/447081-bp-funcs/code/BP%20Funcs.js
// @run-at		document-body
// @noframes
// @grant		GM_addStyle
// @grant		GM_xmlhttpRequest
// @grant		GM_registerMenuCommand
// @grant		GM_getValue
// @grant		GM_setValue
// @connect		*
// ==/UserScript==

// 2.0.0
// - Using GM_Config for settings now (https://github.com/sizzlemctwizzle/GM_config) 🖤
//   (Available from the Tampermonkey menu, under the script name)
// - Added setting to disregard new titles if they're shorter than the old ones. Duh, should have done that from the start ^^
//   (ON by default)
// - The new titles, as well as additional info (old titles, errors, warnings) are now always shown in the element title (mouse hover tooltip), where applicable
// - Moved away from extending the whole layout width in favor of just showing the overflowing new titles.
//   This is to prevent clashes with google widgets and panels that may appear on the right.
//   If anything is covered, the longet titles are now always available on mouse hover over the links.
// - Fixed a potential issue where titles containing special chars could inadvertently render HTML instead of just their escaped text 😬
// - Using my function library again, also for nicer logging
// - moved to classes for styling instead of "spaghetti" element styles
// I dunno, probably some other stuff. I'm an artist, not a book keeper 😜
// 1.9.7:
// added newer CSS variable "center-column-width" for unrestricting column width
// 1.9.6:
// Remove width limit for results column and items
// 1.9.5:
// corrected selectors for color changes
// 1.9.4:
// changed background colors from "very light shades" to "transparent clors", for dark mode

var settings = {};
settings.applyToLinkText = true;
settings.rex = "<title([^>]*)>(.*?)<\\/title>";
settings.dontLookupExtensions = "pdf";
settings.ignoreShorter = true;
settings.ignoreTitles = "^Just a moment...\n^Continue$\n^Untitled$\n^Please wait$\n^Redirecting$\n^Watch$\n/^(log|sign )in$\n^Reddit - Dive into anything/i";
settings.verbosity = 1;

var logger = new BPLogger(GM_info.script.name);

if("undefined" != typeof GM_config){
	GM_config.init(
	{
		'id': 'MyConfig',
		'title': GM_info.script.name + ' Settings',
		'fields': {
			'applyToLinkText': {
				'label': '<b>Replace the actual <i>link text</i> with any found title</b><br /> <i>ON:</i> Change innerHTML of links and applying overflow: visible to parents <br /><i>OFF:</i> Only apply to Link Title (for mouseover Tooltip) <br /><i class="small">Default: ON</i> ',
				'type': 'checkbox',
				'default': true
			},
			'rex': {
				'label': '<b>Regex to find the title of a page.</b> If you find a better way, please <a href="https://greasyfork.org/en/scripts/27406-retrieve-full-page-titles-in-google-search/feedback" target="_blank">let me know</a>! <br /><i class="small">Default: "<title([^>]*)>(.*?)<\\/title>"</i>',
				'type': 'text',
				'default': "<title([^>]*)>(.*?)<\\/title>"
			},
			'dontLookupExtensions': {
				'label': '<b>Exclude file extensions from lookup.</b> For example, links to .pdf file will usually trigger a downlaod when following the link with a GET request. <br /><i class="small">Separate with commas. Default: "pdf',
				'type': 'text',
				'default': "pdf"
			},
			'ignoreShorter': {
				'label': '<b>Ignore new titles that are shorter than the old ones.</b> Titles shorter than the originally truncated ones are probably not what we want. <br /><i class="small">Default: ON</i>',
				'type': 'checkbox',
				'default': true
			},
			'ignoreTitles': {
				'label': '<b>Ignore looked up titles when they return one of these.</b> Usually only needed when the above setting is OFF. <br /><i class="small">Can be RegEx by using /slash delimiters/. Default: <br />Just a moment...<br />Continue<br />Please wait<br />Redirecting<br />Watch<br />/^(log|sign )in$/i</i>',
				'type': 'textarea',
				'default': "Just a moment...\nContinue\nPlease wait\nRedirecting\nWatch\n/^(log|sign )in$/i"
			},
			'verbosity': {
				'label': '<b>Console logging verbosity.</b> 0 = no logs; 1 = reports on link counts; 2 = +statuses of link checks; 3 = +Details <br /><i class="small">Default: 1</i>',
				'type': 'int',
				'default': 1
			}
		},
		'css': `
		#MyConfig{
			background: #333;
			color: #ccc;
			line-height: 1.33em;
		}
		#MyConfig input[type="text"],
		#MyConfig input[type="email"],
		#MyConfig input[type="number"],
		#MyConfig input[type="password"],
		#MyConfig input[type="url"],
		#MyConfig input[type="submit"],
		#MyConfig input[type="reset"],
		#MyConfig input[type="button"],
		#MyConfig select,
		#MyConfig option,
		#MyConfig textarea
		{
			box-sizing: border-box;
			background: #1a1a1a;
			padding: 0.8em;
			color: #fff;
			vertical-align: bottom;
		}
		#MyConfig input[type="text"],
		#MyConfig input[type="email"],
		#MyConfig input[type="number"],
		#MyConfig input[type="password"],
		#MyConfig input[type="url"],
		#MyConfig textarea
		{
			width: 100%;
			border: 2px inset rgba(20,108,128,0.5);
			border-radius: 0.1em;
		}
		#MyConfig input:focus,
		#MyConfig textarea:focus
		{
			color: #fff;
			background: #000;
		}
		#MyConfig textarea
		{
			min-height: 6em;
		}
		#MyConfig .field_label{
			font-weight: normal;
			font-size: 15px;
		}
		#MyConfig kbd{
			border: 1px solid rgba(128,128,128,0.5);
			background: rgba(128,128,128,0.2);
			border-radius: 3px;
			padding: 1px 3px;
			font-family: consolas, monospace;
		}
		#MyConfig .code{
			background: rgba(128,128,128,0.1);
			padding: 1px 3px;
			font-family: consolas, monospace;
		}
		#MyConfig .field_label .small{
			font-size: 80%;
		}
		#MyConfig .config_header{
			margin: 1em 0;
		}
		#MyConfig .config_var{
			border: 1px solid rgba(128,128,128,0.5);
			padding: 1em;
			margin: 0.5em 0 0;
			border-radius: 0.5em;
		}
		#MyConfig a{
			color: #fff;
		}
		#MyConfig .reset, #MyConfig .reset a, #MyConfig_buttons_holder {
			color: #aaa;
		}
		#MyConfig button, #MyConfig .button,
		#MyConfig input[type="submit"],
		#MyConfig input[type="reset"],
		#MyConfig input[type="button"],
		#MyConfig .saveclose_buttons{
			background: rgba(40, 130,180,0.7);
			color: #ddd;
			border: 1px solid transparent;
			cursor: pointer;
			padding: 0.5em 1em;
			border-radius: 0.75em;
			transition: all 0.5s;
		}
		#MyConfig button:hover, #MyConfig .button:hover,
		#MyConfig input[type="submit"]:hover,
		#MyConfig input[type="reset"]:hover,
		#MyConfig input[type="button"]:hover{
			background: rgba(0, 70,100,1);
			color: #fff;
			border-color: rgba(40, 210,255,1);
			border-radius: 0;
		}
		`, 
		events: {
			init: main
		}
	});
}
else{
	log("Could not load GM_config! external resource may be temporarily down?\nUsing default settings for now.", 1, "error");
	
	GM_registerMenuCommand(GM_info.script.name + ' Settings', function(){
		alert("Could not load GM_config! external resource may be temporarily down?\nUsing default settings for now.");
		main(false);
		// TODO: nicer message
		// TODO: Manual lookup
	});
}

logger.level = settings.verbosity;

var myVersion = GM_info.script.version;
var settingsSafety = settings;
var linkmatch = "#search #rso .g a h3";
var resultsObserver;
var idle = true;
var idletimer;
var updaterequest = false;
var openRequests = 0;
var successRequests = 0;
var failedRequests = 0;
var msgPrefix = "Full Page Titles in Google Search:\n";

var lastCount = null;

function log(obj, level = 1, type = "default"){
	if(!(obj instanceof Array))
		obj = [obj];
	logger.writeLog(obj, type, level);
}

log("Verbosity level: " + settings.verbosity, 1);

GM_addStyle(`
/* // Legacy, old version. 
#rso div.g{
	width:auto!important;
}
.srp {
    --center-column: auto;
    --center-column-width: auto;
	--center-width: auto;
}
*/
.showverflow{
	overflow: visible!important;
	contain: layout!important;
}
.imgFix{
    position: relative;
    top: 4em;
}
.titlesChecking{
	background-color: rgba(255, 200, 0, 0.1);
}
.titlesFail{
	background-color: rgba(255, 0, 0, 0.1);
}
.titlesOK{
	background-color: rgba(30, 255, 0, 0.1);
}
`);

var rexRex = /\/(.+)\/([dgimsuy]*)/;

for(let i=0; i<settings.ignoreTitles.length; i++){
	let x = settings.ignoreTitles[i];
	let m = rexRex.exec(x);
	if(m && m.length>1){
		try{
			let rex = new RegExp(m[1], m[2]);
			settings.ignoreTitles[i] = rex;
		}
		catch(e){
			log("Not a valid regular expression in settings.ignoreTitles: " + x, 1, "error");
		}
	}
}

function updatePage(){
    var allinks = 0;
    log("Walking through Links...", 2);
	$(linkmatch + ":not([titleDone], .titlesChecking)").each(function(){
        allinks ++;
//        log(this, 3);
//        log("Looking at Link '" + $(this).parent()[0].href + "' (" + this.innerHTML + ")", 3);
		if(this.textContent.substr(this.textContent.length-3)=="..."){
			log("'" + this.textContent + "' Needs checking", 2);
			$(this).addClass("titlesChecking");
			getTitle(this);
		}
	});
	if(lastCount !== openRequests)
		log(openRequests + " of " + allinks + " links need to be checked.", 1);
	lastCount = openRequests;
}

function getTitle(el){
	var a = $(el).closest("a")[0];
    log("Title '" + a.href + "' (" + el.textContent + ") looks shortened.", 3);
	for(var i=0; i<settings.dontLookupExtensions.length; i++)
	{
		if(a.href.endson(settings.dontLookupExtensions[i], true)){
            log("Excluding Link '" + a.href + "' (" + el.textContent + ") because the extension (" + settings.dontLookupExtensions[i] + "), which is excluded", 3);
			el.title = "The extension " + settings.dontLookupExtensions[i] + " is excluded in settings";
			$(el).removeClass("titlesChecking");
			$(el).addClass("titlesFail");
			$(el).attr("titleDone", "true");
			return;
        }
	}
    openRequests++;
	$(el).addClass("titlesChecking");
	GM_xmlhttpRequest({
		url: a.href,
		method: "GET",
		timeout: 15000, //15 seconds timeout
		onload: function(res){
			$(el).attr("titleDone", "true");
			$(el).removeClass("titlesChecking");
            var mrex = new RegExp(settings.rex, "i");
			var tit = mrex.exec(res.response);
			if(tit === undefined || tit === null){
                log("No title found in response for " + a.href, 2, "error");
				el.title = "Failed to get title: No title found in response";
				$(el).addClass("titlesFail");
                report("fail");
				return;
            }
			tit = unEscapeHtml(tit[2]);
			var oldTitle = $(el).text().trim();
			if(settings.ignoreShorter && tit.length < oldTitle.length-3){
				log("Ignoring title '" + tit + "' because it is shorter than the original (" + oldTitle + ")", 3, "warn");
				el.title = "(Ignore:) " + tit;
				$(el).addClass("titlesFail");
				report("success");
				return;
			}
			for(let x of settings.ignoreTitles){
				if((x instanceof RegExp && tit.match(x)) || (typeof x == "string" && x.toLowerCase() == tit.toLowerCase())){
					log(["Ignoring title '" + tit + "' because of the excludion rule", x, "specified in settings"], 3, "warn");
					el.title = "(Ignore:) " + tit;
					$(el).addClass("titlesFail");
					report("success");
					return;
				}
			}
			$(el).addClass("titlesOK");
            if(settings.applyToLinkText){
                $(el).text(tit);
				el.title = tit + "\n\nOriginal Title: " + oldTitle;
                $(a).css("white-space", "nowrap");
                $(a).parentsUntil(".g").last().find("[data-content-feature] img").parents("[data-content-feature]").first().addClass("imgFix");
                $(a).parentsUntil("#main", ":not(.showverflow)").addClass("showverflow");
            }
			else
				el.title = tit;
            report("success");
		},
		onerror: function(res){
			$(el).attr("titleDone", "true");
			$(el).removeClass("titlesChecking");
			$(el).addClass("titlesFail");
			el.title = "Failed to get full title: Error loading page";
			log({error: "Error loading page", link: el}, 2, "error");
            report("fail");
		},
		ontimeout: function(res){
			$(el).attr("titleDone", "true");
			$(el).removeClass("titlesChecking");
			$(el).addClass("titlesFail");
			el.title = "Failed to get full title: Connection timed out";
			log({error: "Connection timed out", link: el}, 2, "error");
            report("fail");
		}
	});
}

function report(status){
    switch(status){
        case "success":
            successRequests ++;
            openRequests --;
            break;
        case "fail":
            failedRequests ++;
            openRequests --;
            break;
    }
    log(successRequests + " requests successful, " + failedRequests + " failed. " + openRequests + " Requests open.", 1);
}

function unEscapeHtml(text){
	var t = document.createElement("TEXTAREA");
	t.innerHTML = text;
	return t.value;
}

String.prototype.endson = function(str, insensitive){
  return new RegExp("("+escapeRegExp(str)+")$", insensitive?"i":"").test(this);
};

function escapeRegExp(str) {
	return str.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&');
}

function updater(t = 1000){
	if(idle)
	{
		idle = false;
		updaterequest = false;
		updatePage();
		idletimer = setTimeout(function(){
			idle = true;
			if(updaterequest)
				updatePage();
		}, t);
	}
	else
	{
        log("Updater called but busy",3);
		updaterequest = true;
	}
}

function main(hasSettings=true){
	if(hasSettings){
		GM_registerMenuCommand(GM_info.script.name + ' Settings', function(){
			GM_config.open();
		});

		settings.applyToLinkText = GM_config.get("applyToLinkText");
		settings.rex = GM_config.get("rex");
		settings.dontLookupExtensions = GM_config.get("dontLookupExtensions").split(",").map(a=>"." + a.trim());
		settings.ignoreShorter = GM_config.get("ignoreShorter");
		settings.ignoreTitles = GM_config.get("ignoreTitles").split(/\r?\n/);
		settings.verbosity = GM_config.get("verbosity");
	}
	log("Start updater interval", 3);
	setInterval(updater, 2000);
}

/* jshint loopfunc: true, -W027 */
/* eslint-disable curly, no-redeclare */
/* eslint no-trailing-spaces: off */
/* globals $, GM_info, GM_setValue, GM_getValue, GM_xmlhttpRequest, GM_addStyle, GM_openInTab, GM_setClipboard, GM_config, escape, uneval, unsafeWindow, BPLogger_default, log, error, warn, getParam, waitFor, BPLogger */