Retrieve Full Page Titles in Google Search

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

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 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 */