Linkify Plus Plus

Based on Linkify Plus. Turn plain text URLs into links.

Versión del día 2/7/2015. Echa un vistazo a la versión más reciente.

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

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

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Necesitará instalar una extensión como Tampermonkey para instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==UserScript==
// @name        Linkify Plus Plus
// @version     6.0.1
// @namespace   eight04.blogspot.com
// @description Based on Linkify Plus. Turn plain text URLs into links.
// @include     http*
// @exclude     http://www.google.*/search*
// @exclude     https://www.google.*/search*
// @exclude     http://www.google.*/webhp*
// @exclude     https://www.google.*/webhp*
// @exclude     http://music.google.*/*
// @exclude     https://music.google.*/*
// @exclude     http://mail.google.*/*
// @exclude     https://mail.google.*/*
// @exclude     http://docs.google.*/*
// @exclude     https://docs.google.*/*
// @exclude     http://mxr.mozilla.org/*
// @require     https://greasyfork.org/scripts/7212-gm-config-eight-s-version/code/GM_config%20(eight's%20version).js?version=57385
// @grant       GM_addStyle
// @grant       GM_registerMenuCommand
// @grant       GM_getValue
// @grant       GM_setValue
// @grant       unsafeWindow
// @compatible  firefox
// @compatible  chrome
// @compatible  opera
// ==/UserScript==

"use strict";

var config,
	re = {
		image: /^[^?#]+\.(?:jpg|png|gif|jpeg)(?:$|[?#])/i
	},
	tlds = {"ac":0,"academy":0,"active":0,"ad":0,"ae":0,"aero":0,"af":0,"ag":0,"agency":0,"ai":0,"al":0,"alsace":0,"am":0,"an":0,"ao":0,"aq":0,"ar":1,"archi":0,"army":0,"arpa":0,"as":0,"asia":0,"associates":0,"at":0,"au":2,"auction":0,"audio":0,"autos":0,"aw":0,"ax":0,"axa":0,"az":0,"ba":0,"bar":0,"bargains":0,"bayern":0,"bb":0,"bd":0,"be":1,"beer":0,"berlin":0,"best":0,"bf":0,"bg":0,"bh":0,"bi":0,"bid":0,"bike":0,"bio":0,"biz":0,"bj":0,"black":0,"blackfriday":0,"blue":0,"bm":0,"bn":0,"bnpparibas":0,"bo":0,"boutique":0,"br":4,"brussels":0,"bs":0,"bt":0,"budapest":0,"build":0,"builders":0,"business":0,"buzz":0,"bv":0,"bw":0,"by":0,"bz":0,"bzh":0,"ca":1,"cab":0,"camera":0,"camp":0,"capetown":0,"capital":0,"caravan":0,"cards":0,"care":0,"careers":0,"casa":0,"cash":0,"cat":0,"catering":0,"cc":0,"cd":0,"center":0,"ceo":0,"cern":0,"cf":0,"cg":0,"ch":1,"channel":0,"christmas":0,"church":0,"ci":0,"city":0,"ck":0,"cl":0,"click":0,"clinic":0,"clothing":0,"club":0,"cm":0,"cn":2,"co":1,"codes":0,"coffee":0,"cologne":0,"com":13,"community":0,"company":0,"computer":0,"condos":0,"construction":0,"consulting":0,"contractors":0,"cooking":0,"cool":0,"coop":0,"country":0,"cr":0,"credit":0,"creditcard":0,"cu":0,"cv":0,"cw":0,"cx":0,"cy":0,"cymru":0,"cz":0,"dance":0,"dating":0,"day":0,"de":4,"deals":0,"dental":0,"desi":0,"diamonds":0,"diet":0,"digital":0,"direct":0,"directory":0,"discount":0,"dj":0,"dk":0,"dm":0,"do":0,"domains":0,"dz":0,"ec":0,"edu":1,"education":0,"ee":0,"eg":0,"email":0,"engineer":0,"engineering":0,"enterprises":0,"equipment":0,"er":0,"es":0,"estate":0,"et":0,"eu":0,"eus":0,"events":0,"exchange":0,"expert":0,"exposed":0,"fail":0,"farm":0,"feedback":0,"fi":0,"fish":0,"fishing":0,"fitness":0,"fj":0,"fk":0,"florist":0,"fly":0,"fm":0,"fo":0,"foo":0,"forsale":0,"foundation":0,"fr":2,"frl":0,"frogans":0,"fund":0,"furniture":0,"futbol":0,"ga":0,"gal":0,"gallery":0,"gb":0,"gd":0,"ge":0,"gent":0,"gf":0,"gg":0,"gh":0,"gi":0,"gift":0,"gifts":0,"gl":0,"glass":0,"global":0,"gm":0,"gn":0,"gop":0,"gov":0,"gp":0,"gq":0,"gr":0,"graphics":0,"green":0,"gs":0,"gt":0,"gu":0,"guide":0,"guru":0,"gw":0,"gy":0,"hamburg":0,"haus":0,"help":0,"here":0,"hiv":0,"hk":0,"hm":0,"hn":0,"holdings":0,"holiday":0,"homes":0,"horse":0,"host":0,"hosting":0,"house":0,"hr":0,"ht":0,"hu":0,"id":0,"ie":0,"il":0,"im":0,"immo":0,"in":1,"industries":0,"info":0,"ink":0,"institute":0,"insure":0,"int":0,"international":0,"io":0,"iq":0,"ir":0,"is":0,"it":3,"je":0,"jetzt":0,"jm":0,"jo":0,"jobs":0,"jp":7,"kaufen":0,"ke":0,"kg":0,"kh":0,"ki":0,"kim":0,"kitchen":0,"kiwi":0,"km":0,"kn":0,"koeln":0,"kp":0,"kr":0,"kred":0,"kw":0,"ky":0,"kz":0,"la":0,"land":0,"lb":0,"lc":0,"lease":0,"lgbt":0,"li":0,"life":0,"lighting":0,"limited":0,"limo":0,"link":0,"lk":0,"loans":0,"london":0,"lotto":0,"lr":0,"ls":0,"lt":0,"ltda":0,"lu":0,"luxe":0,"lv":0,"ly":0,"ma":0,"maison":0,"management":0,"market":0,"marketing":0,"mc":0,"md":0,"me":0,"media":0,"meet":0,"melbourne":0,"menu":0,"mg":0,"mh":0,"miami":0,"mil":0,"mk":0,"ml":0,"mm":0,"mn":0,"mo":0,"mobi":0,"moda":0,"moe":0,"moscow":0,"motorcycles":0,"mp":0,"mq":0,"mr":0,"ms":0,"mt":0,"mu":0,"museum":0,"mv":0,"mw":0,"mx":2,"my":0,"mz":0,"na":0,"nagoya":0,"name":0,"nc":0,"ne":0,"net":38,"network":0,"neustar":0,"new":0,"nexus":0,"nf":0,"ng":0,"ni":0,"ninja":0,"nl":1,"no":0,"np":0,"nr":0,"nra":0,"nrw":0,"nu":0,"nyc":0,"nz":0,"om":0,"ong":0,"onl":0,"ooo":0,"org":0,"organic":0,"ovh":0,"pa":0,"paris":0,"partners":0,"parts":0,"pe":0,"pf":0,"pg":0,"ph":0,"pharmacy":0,"photo":0,"photography":0,"photos":0,"pics":0,"pictures":0,"pink":0,"pk":0,"pl":1,"place":0,"plumbing":0,"pm":0,"pn":0,"post":0,"pr":0,"praxi":0,"press":0,"pro":0,"prod":0,"productions":0,"properties":0,"ps":0,"pt":0,"pub":0,"pw":0,"py":0,"qa":0,"qpon":0,"quebec":0,"re":0,"realtor":0,"recipes":0,"red":0,"reise":0,"reisen":0,"rentals":0,"repair":0,"report":0,"republican":0,"rest":0,"restaurant":0,"reviews":0,"rich":0,"rio":0,"ro":0,"rocks":0,"rodeo":0,"rs":0,"ru":1,"ruhr":0,"rw":0,"sa":0,"saarland":0,"sarl":0,"sb":0,"sc":0,"scb":0,"scot":0,"sd":0,"se":1,"services":0,"sexy":0,"sg":0,"sh":0,"shiksha":0,"shoes":0,"si":0,"singles":0,"sk":0,"sl":0,"sm":0,"sn":0,"so":0,"social":0,"software":0,"solar":0,"solutions":0,"soy":0,"space":0,"sr":0,"st":0,"su":0,"supply":0,"support":0,"surf":0,"sv":0,"sx":0,"sy":0,"systems":0,"sz":0,"tatar":0,"tattoo":0,"tax":0,"tc":0,"td":0,"technology":0,"tel":0,"tf":0,"tg":0,"th":0,"tips":0,"tirol":0,"tj":0,"tk":0,"tl":0,"tm":0,"tn":0,"to":0,"today":0,"tokyo":0,"tools":0,"town":0,"tp":0,"tr":1,"trade":0,"training":0,"travel":0,"tt":0,"tv":0,"tw":1,"tz":0,"ua":0,"ug":0,"uk":1,"university":0,"unknown":0,"uno":0,"us":0,"uy":0,"uz":0,"va":0,"vc":0,"ve":0,"vegas":0,"ventures":0,"versicherung":0,"vg":0,"vi":0,"villas":0,"vision":0,"vlaanderen":0,"vn":0,"vodka":0,"vote":0,"voting":0,"voto":0,"voyage":0,"vu":0,"wang":0,"watch":0,"webcam":0,"website":0,"wed":0,"wf":0,"whoswho":0,"wien":0,"wiki":0,"williamhill":0,"work":0,"works":0,"world":0,"ws":0,"wtf":0,"xn--3ds443g":0,"xn--4gbrim":0,"xn--55qx5d":0,"xn--6frz82g":0,"xn--80adxhks":0,"xn--80ao21a":0,"xn--80asehdb":0,"xn--80aswg":0,"xn--c1avg":0,"xn--d1acj3b":0,"xn--fiq228c5hs":0,"xn--fiqs8s":0,"xn--fiqz9s":0,"xn--i1b6b1a6a2e":0,"xn--j1amh":0,"xn--j6w193g":0,"xn--kpry57d":0,"xn--kput3i":0,"xn--mgbaam7a8h":0,"xn--mgberp4a5d4ar":0,"xn--ngbc5azd":0,"xn--nqv7f":0,"xn--nqv7fs00ema":0,"xn--p1acf":0,"xn--p1ai":0,"xn--q9jyb4c":0,"xn--rhqv96g":0,"xn--ses554g":0,"xxx":0,"xyz":0,"yachts":0,"yandex":0,"ye":0,"yokohama":0,"yt":0,"za":0,"zm":0,"zone":0,"zw":0,"在线":0,"موقع":0,"公司":0,"移动":0,"москва":0,"қаз":0,"онлайн":0,"сайт":0,"орг":0,"дети":0,"中文网":0,"中国":0,"中國":0,"संगठन":0,"укр":0,"香港":0,"台灣":0,"手机":0,"امارات":0,"السعودية":0,"شبكة":0,"机构":0,"组织机构":0,"рус":0,"рф":0,"みんな":0,"世界":0,"网址":0},
	selectors,
	que = [],
	thread = createThread(queGen);

initConfig({
	image: {
		label: "Embed images",
		type: "checkbox",
		default: true
	},
	unicode: {
		label: "Allow non-ascii character",
		type: "checkbox",
		default: false
	},
	ignoreTags: {
		label: "Do not linkify urls in these tags",
		type: "textarea",
		default: "a noscript option script style textarea svg canvas button select template meter progress math h1 h2 h3 h4 h5 h6 time code"
	},
	ignoreClasses: {
		label: "Do not linkify urls in these classes",
		type: "textarea",
		default: "highlight editbox brush: bdsug spreadsheetinfo"
	},
	selectors: {
		label: "Always linkify these elements. One CSS selector per line.",
		type: "textarea",
		default: ""
	},
	newTab: {
		label: "Open link in new tab",
		type: "checkbox",
		default: false
	}
});

function initConfig(options) {
	GM_config.init(GM_info.script.name, options);
	GM_config.onclose = loadConfig;
	loadConfig();
	GM_registerMenuCommand(GM_info.script.name + " - Configure", GM_config.open);
}

function loadConfig(){
	config = GM_config.get();

	selectors = config.selectors.trim().replace(/\n/, ", ");

	var arr;

	arr = getArray(config.ignoreTags);
	if (arr) {
		re.ignoreTags = new RegExp("^(" + arr.join("|") + ")$", "i");
	} else {
		re.ignoreTags = null;
	}

	arr = getArray(config.ignoreClasses);
	if (arr) {
		re.ignoreClasses = new RegExp("(^|\\s)(" + arr.join("|") + ")($|\\s)");
	} else {
		re.ignoreClasses = null;
	}

	// 1=protocol, 2=user, 3=domain, 4=port, 5=path, 6=angular source
	if (config.unicode) {
		re.url = /\b([-a-z*]+:\/\/)?(?:([\w:.+-]+)@)?([a-z0-9-.\u00b7-\u2a6d6]+\.[a-z0-9-авгдезийклмнорстуфқابةتدرسشعقكلمويंगठनसなみん世中公动台司国國在址手文机构港灣界移线组织网香]{1,17})\b(:\d+)?([/?#]\S*)?|\{\{(.+?)\}\}/gi;
	} else {
		re.url = /\b([-a-z*]+:\/\/)?(?:([\w:.+-]+)@)?([a-z0-9-.]+\.[a-z0-9-]{1,17})\b(:\d+)?([/?#][\w-.~!$&*+;=:@%/?#(),'\[\]]*)?|\{\{(.+?)\}\}/gi;
	}
}

function valid(node) {
	var className = node.className;
	if (typeof className == "object") {
		className = className.baseVal;
	}
	if (re.ignoreTags && re.ignoreTags.test(node.nodeName)) {
		return false;
	}
	if (className && re.ignoreClasses && re.ignoreClasses.test(className)) {
		return false;
	}
	if (node.contentEditable == "true" || node.contentEditable == "") {
		return false;
	}
	if (className && className.indexOf("linkifyplus") >= 0) {
		return false;
	}
	return true;
}

var nodeFilter = {
	acceptNode: function(node) {
		if (!valid(node)) {
			return NodeFilter.FILTER_REJECT;
		}
		if (node.nodeName == "WBR") {
			return NodeFilter.FILTER_ACCEPT;
		}
		if (node.nodeType == 3) {
			return NodeFilter.FILTER_ACCEPT;
		}
		return NodeFilter.FILTER_SKIP;
	}
};

function createThread(gen, done) {
	var running = false,
		timeout,
		chunks,
		iter;

	function start(param) {
		if (running) {
			return;
		}
		chunks = 0;
		running = true;
		iter = gen(param);
		timeout = setTimeout(next);
	}

	function next() {
		chunks++;
		var count = 0, done;
		while (!(done = iter.next().done) && count < 20) {
			count++;
		}
		if (!done) {
			timeout = setTimeout(next);
		} else {
			stop();
		}
	}

	function stop() {
		running = false;
		clearTimeout(timeout);
		if (done) {
			done();
		}
	}

	return {
		start: start,
		stop: stop
	};
}

function validRoot(node) {
	if (node.VALID !== undefined) {
		return node.VALID;
	}
	var cache = [], isValid;
	while (node != document.documentElement) {
		cache.push(node);
		if (!valid(node)) {
			isValid = false;
			break;
		}
		if (!node.parentNode) {
			return false;
		}
		node = node.parentNode;
		if (node.VALID !== undefined) {
			isValid = node.VALID;
			break;
		}
	}
	if (isValid === undefined) {
		isValid = true;
	}
	var i;
	for (i = 0; i < cache.length; i++) {
		cache[i].VALID = isValid;
	}
	return isValid;
}

function queAdd(node) {
	if (node.QUE_COUNT === undefined) {
		node.QUE_COUNT = 0;
	}

	node.QUE_COUNT++;
	que.push(node);
}

function* queGen () {
	// Generate linkified range from que.
	var node;
	while ((node = que.shift())) {
		node.QUE_COUNT--;
		if (node.QUE_COUNT > 0) {
			continue;
		}
		yield* createTreeWalker(node);
	}
}

function getArray(s) {
	s = s.trim();
	if (!s) {
		return null;
	}
	return s.split(/\s+/);
}

function isIP(s) {
	var m, i;
	if (!(m = s.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/))) {
		return false;
	}
	for (i = 1; i < m.length; i++) {
		if (+m[i] > 255 || (m[i].length > 1 && m[i][0] == "0")) {
			return false;
		}
	}
	return true;
}

var createRe = function(){
	var pool = {};

	return function (str, flags) {
		if (!(str in pool)) {
			pool[str] = new RegExp(str, flags);
		}
		// Reset RE
		pool[str].lastIndex = 0;
		return pool[str];
	};
}();

function stripSingleSymbol(str, left, right) {
	var re = createRe("[\\" + left + "\\" + right + "]", "g"),
		match, count = 0, end;

	// Match loop
	while ((match = re.exec(str))) {
		if (count % 2 == 0) {
			end = match.index;
			if (match[0] == right) {
				break;
			}
		} else {
			if (match[0] == left) {
				break;
			}
		}
		count++;
	}

	if (!match && count % 2 == 0) {
		return str;
	}

	return str.substr(0, end);
}

function createLink(url, child) {
	var cont = document.createElement("a");
	cont.href = url;
	cont.title = "Linkify Plus Plus";
	if (config.newTab) {
		cont.target = "_blank";
	}
	if (config.image && re.image.test(url)) {
		child = new Image;
		child.src = url;
		child.alt = url;
	}
	cont.appendChild(child);
	cont.className = "linkifyplus";

	return cont;
}

function replaceRange(range, nodes) {
	var i, j;

	// Get text targets
	var targets = [],
		list = range.startContainer.childNodes,
		offset = 0,
		endOffset = 0;
	for (i = range.startOffset; i < range.endOffset; i++) {
		if (list[i].nodeType == 3) {
			endOffset = offset + list[i].nodeValue.length;
		}
		targets.push({
			offset: offset,
			endOffset: endOffset,
			node: list[i]
		});
		offset = endOffset;
	}

	// Compare offset with range position
	var subRange = document.createRange(),
		frag = document.createDocumentFragment(),
		text;
	for (i = 0, j = 0; i < nodes.length; i++) {
		// Create sub range
		while (nodes[i].start >= targets[j].endOffset) {
			j++;
		}
		subRange.setStart(targets[j].node, nodes[i].start - targets[j].offset);
		while (nodes[i].end > targets[j].endOffset) {
			j++;
		}
		subRange.setEnd(targets[j].node, nodes[i].end - targets[j].offset);

		// Create text and link
		text = subRange.cloneContents();
		if (nodes[i].type == "string") {
			frag.appendChild(text);
		} else {
			frag.appendChild(createLink(nodes[i].url, text));
		}
	}

	// Replace range
	range.deleteContents();
	range.insertNode(frag);
}

function linkifyTextNode(range) {
	var m, mm,
		txt = range.toString(),
		nodes = [],
		lastIndex = 0;
	var face, protocol, user, domain, port, path, angular;
	var url;

	while (m = re.url.exec(txt)) {
		face = m[0];
		protocol = m[1] || "";
		user = m[2] || "";
		domain = m[3] || "";
		port = m[4] || "";
		path = m[5] || "";
		angular = m[6];

		// Skip angular source
		if (angular) {
			if (!unsafeWindow.angular) {
				re.url.lastIndex = m.index + 2;
			}
			continue;
		}

		// domain shouldn't contain connected dots
		if (domain.indexOf("..") > -1) {
			continue;
		}

		// valid IP address
		if (!isIP(domain) && (mm = domain.match(/\.([a-z0-9-]+)$/i)) && !(mm[1].toLowerCase() in tlds)) {
			continue;
		}

		// Insert text
		if (m.index > lastIndex) {
			nodes.push({
				start: lastIndex,
				end: m.index,
				type: "string"
			});
		}

		if (path) {
			// Remove trailing dots and comma
			face = face.replace(/[.,]*$/, '');
			path = path.replace(/[.,]*$/, '');

			// Strip parens "()"
			face = stripSingleSymbol(face, "(", ")");
			path = stripSingleSymbol(path, "(", ")");

			// Strip bracket "[]"
			face = stripSingleSymbol(face, "[", "]");
			path = stripSingleSymbol(path, "[", "]");
		}

		// Guess protocol
		if (!protocol && user && (mm = user.match(/^mailto:(.+)/))) {
			protocol = "mailto:";
			user = mm[1];
		}

		if (protocol && protocol.match(/^(hxxp|h\*\*p|ttp)/)) {
			protocol = "http://";
		}

		if (!protocol) {
			if (mm = domain.match(/^(ftp|irc)/)) {
				protocol = mm[0] + "://";
			} else if (domain.match(/^(www|web)/)) {
				protocol = "http://";
			} else if (user && user.indexOf(":") < 0 && !path) {
				protocol = "mailto:";
			} else {
				protocol = "http://";
			}
		}

		// Create URL
		url = protocol + (user && user + "@") + domain + port + path;

		re.url.lastIndex = m.index + face.length;
		lastIndex = re.url.lastIndex;

		nodes.push({
			start: m.index,
			end: lastIndex,
			type: "anchor",
			url: url
		});
	}

	if (!nodes.length) {
		return;
	}

	if (txt.length > lastIndex) {
		nodes.push({
			start: lastIndex,
			end: txt.length,
			type: "string"
		});
	}

	replaceRange(range, nodes);
}

function* createTreeWalker(node) {
	// Generate linkified ranges.
	var walker = document.createTreeWalker(
		node,
		NodeFilter.SHOW_TEXT + NodeFilter.SHOW_ELEMENT,
		nodeFilter
	), start, end, current, range;

	end = start = walker.nextNode();
	if (!start) {
		return;
	}
	range = document.createRange();
	range.setStartBefore(start);
	while ((current = walker.nextNode())) {
		if (end.nextSibling == current) {
			end = current;
			continue;
		}
		range.setEndAfter(end);
		yield linkifyTextNode(range);

		end = start = current;
		range = document.createRange();
		range.setStartBefore(start);
	}
	range.setEndAfter(end);
	yield linkifyTextNode(range);
}

function* mutationGen(mutations) {
	// Generate nodes
	var i;
	for (i = 0; i < mutations.length; i++) {
		if (mutations[i].addedNodes.length) {
			yield processNode(mutations[i].target);
		}
	}
}

function processNode(node) {
	if (validRoot(node)) {
		queAdd(node);
	}
	if (selectors) {
		Array.prototype.forEach.call(node.querySelectorAll(selectors), queAdd);
	}
}

GM_addStyle(".linkifyplus img { max-width: 90%; }");

new MutationObserver(function(mutations){
	createThread(mutationGen, thread.start).start(mutations);
}).observe(document.body, {
	childList: true,
	subtree: true
});

processNode(document.body);
thread.start();