Linkify Plus Plus

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

Устаревшая версия за 27.04.2015. Перейдите к последней версии.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey, Greasemonkey или Violentmonkey.

Для установки этого скрипта вам необходимо установить расширение, такое как Tampermonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Violentmonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Userscripts.

Чтобы установить этот скрипт, сначала вы должны установить расширение браузера, например Tampermonkey.

Чтобы установить этот скрипт, вы должны установить расширение — менеджер скриптов.

(у меня уже есть менеджер скриптов, дайте мне установить скрипт!)

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

(у меня уже есть менеджер стилей, дайте мне установить скрипт!)

// ==UserScript==
// @name        Linkify Plus Plus
// @version     4.0.0
// @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=46603
// @grant       GM_addStyle
// @grant       GM_registerMenuCommand
// @grant       GM_getValue
// @grant       GM_setValue
// ==/UserScript==

"use strict";

var config, re;

GM_config.init(
	"Linkify Plus Plus",
	{
		useImg: {
			label: "Embed images",
			type: "checkbox",
			default: true
		},
		classBlackList: {
			label: "Add classes to black-list (Separate by space)",
			type: "textarea",
			default: ""
		},
		classWhiteList: {
			label: "Add classes to white-list (Separate by space)",
			type: "textarea",
			default: ""
		},
		generateLog: {
			label: "Generate log (useful for debugging)",
			type: "checkbox",
			default: false
		},
		openInNewTab: {
			label: "Open link in new tab",
			type: "checkbox",
			default: false
		},
		useYT: {
			label: "Embed youtube video",
			type: "checkbox",
			default: false
		},
		ytWidth: {
			label: "Video width",
			type: "text",
			default: "560"
		},
		ytHeight: {
			label: "Video height",
			type: "text",
			default: "315"
		},
		embedAll: {
			label: "Enable embedding globally (It might mess up your page!)",
			type: "checkbox",
			default: false
		}
	}
);

GM_config.onclose = loadConfig;

loadConfig();

// 1=protocol, 2=user, 3=domain, 4=port, 5=path, 6=angular source
var urlRE = /\b([-a-z*]+:\/\/)?(?:([\w:.+-]+)@)?([a-z0-9-.]+\.[a-z0-9-]+)\b(:\d+)?([/?#][\w-.~!$&*+;=:@%/?#(),]*)?|\{\{(.+?)\}\}/gi;

var tlds = {
	"AC":true,"ACADEMY":false,"ACTIVE":false,"AD":true,"AE":true,"AERO":true,"AF":true,"AG":true,"AGENCY":false,"AI":true,"AL":true,"ALSACE":false,"AM":true,"AN":true,"AO":true,"AQ":true,"AR":true,"ARCHI":false,"ARMY":false,"ARPA":true,"AS":true,"ASIA":true,"ASSOCIATES":false,"AT":true,"AU":true,"AUCTION":false,"AUDIO":false,"AUTOS":false,"AW":true,"AX":true,"AXA":false,"AZ":true,"BA":true,"BAR":false,"BARGAINS":false,"BAYERN":false,"BB":true,"BD":true,"BE":true,"BEER":false,"BERLIN":false,"BEST":false,"BF":true,"BG":true,"BH":true,"BI":true,"BID":false,"BIKE":false,"BIO":false,"BIZ":true,"BJ":true,"BLACK":false,"BLACKFRIDAY":false,"BLUE":false,"BM":true,"BN":true,"BNPPARIBAS":false,"BO":true,"BOUTIQUE":false,"BR":true,"BRUSSELS":false,"BS":true,"BT":true,"BUDAPEST":false,"BUILD":false,"BUILDERS":false,"BUSINESS":false,"BUZZ":false,"BV":false,"BW":true,"BY":true,"BZ":true,"BZH":false,"CA":true,"CAB":false,"CAMERA":false,"CAMP":true,"CAPETOWN":false,"CAPITAL":false,"CARAVAN":false,"CARDS":false,"CARE":false,"CAREERS":false,"CASA":false,"CASH":false,"CAT":true,"CATERING":false,"CC":true,"CD":true,"CENTER":true,"CEO":false,"CERN":false,"CF":true,"CG":false,"CH":true,"CHANNEL":false,"CHRISTMAS":false,"CHURCH":false,"CI":true,"CITY":false,"CK":true,"CL":true,"CLICK":false,"CLINIC":false,"CLOTHING":false,"CLUB":true,"CM":true,"CN":true,"CO":true,"CODES":false,"COFFEE":false,"COLOGNE":false,"COM":true,"COMMUNITY":false,"COMPANY":false,"COMPUTER":false,"CONDOS":false,"CONSTRUCTION":false,"CONSULTING":false,"CONTRACTORS":false,"COOKING":false,"COOL":false,"COOP":true,"COUNTRY":false,"CR":true,"CREDIT":false,"CREDITCARD":false,"CU":true,"CV":true,"CW":true,"CX":true,"CY":true,"CYMRU":false,"CZ":true,"DANCE":false,"DATING":false,"DAY":false,"DE":true,"DEALS":false,"DENTAL":false,"DESI":false,"DIAMONDS":false,"DIET":false,"DIGITAL":false,"DIRECT":false,"DIRECTORY":false,"DISCOUNT":false,"DJ":true,"DK":true,"DM":true,"DO":true,"DOMAINS":false,"DZ":true,"EC":true,"EDU":true,"EDUCATION":false,"EE":true,"EG":true,"EMAIL":true,"ENGINEER":false,"ENGINEERING":false,"ENTERPRISES":false,"EQUIPMENT":false,"ER":true,"ES":true,"ESTATE":false,"ET":true,"EU":true,"EUS":false,"EVENTS":false,"EXCHANGE":true,"EXPERT":false,"EXPOSED":false,"FAIL":false,"FARM":false,"FEEDBACK":false,"FI":true,"FISH":false,"FISHING":false,"FITNESS":false,"FJ":true,"FK":true,"FLORIST":false,"FLY":false,"FM":true,"FO":true,"FOO":false,"FORSALE":false,"FOUNDATION":false,"FR":true,"FRL":false,"FROGANS":false,"FUND":false,"FURNITURE":false,"FUTBOL":false,"GA":true,"GAL":false,"GALLERY":false,"GB":false,"GD":true,"GE":true,"GENT":false,"GF":true,"GG":true,"GH":true,"GI":true,"GIFT":false,"GIFTS":false,"GL":true,"GLASS":false,"GLOBAL":true,"GM":true,"GN":false,"GOP":false,"GOV":true,"GP":true,"GQ":true,"GR":true,"GRAPHICS":false,"GREEN":false,"GS":true,"GT":true,"GU":false,"GUIDE":false,"GURU":true,"GW":false,"GY":true,"HAMBURG":false,"HAUS":false,"HELP":false,"HERE":false,"HIV":false,"HK":true,"HM":true,"HN":true,"HOLDINGS":false,"HOLIDAY":false,"HOMES":false,"HORSE":false,"HOST":true,"HOSTING":true,"HOUSE":false,"HR":true,"HT":true,"HU":true,"ID":true,"IE":true,"IL":true,"IM":true,"IMMO":false,"IN":true,"INDUSTRIES":false,"INFO":true,"INK":false,"INSTITUTE":false,"INSURE":false,"INT":true,"INTERNATIONAL":false,"IO":true,"IQ":false,"IR":true,"IS":true,"IT":true,"JE":true,"JETZT":false,"JM":true,"JO":true,"JOBS":false,"JP":true,"KAUFEN":false,"KE":true,"KG":true,"KH":true,"KI":false,"KIM":false,"KITCHEN":false,"KIWI":false,"KM":false,"KN":false,"KOELN":false,"KP":false,"KR":true,"KRED":false,"KW":true,"KY":true,"KZ":true,"LA":true,"LAND":false,"LB":true,"LC":true,"LEASE":false,"LGBT":false,"LI":true,"LIFE":false,"LIGHTING":false,"LIMITED":false,"LIMO":false,"LINK":true,"LK":true,"LOANS":false,"LONDON":true,"LOTTO":false,"LR":true,"LS":true,"LT":true,"LTDA":false,"LU":true,"LUXE":false,"LV":true,"LY":true,"MA":true,"MAISON":false,"MANAGEMENT":false,"MARKET":false,"MARKETING":false,"MC":true,"MD":true,"ME":true,"MEDIA":false,"MEET":false,"MELBOURNE":false,"MENU":false,"MG":true,"MH":true,"MIAMI":false,"MIL":true,"MK":true,"ML":true,"MM":true,"MN":true,"MO":true,"MOBI":true,"MODA":false,"MOE":true,"MOSCOW":false,"MOTORCYCLES":false,"MP":false,"MQ":false,"MR":false,"MS":true,"MT":true,"MU":true,"MUSEUM":false,"MV":true,"MW":true,"MX":true,"MY":true,"MZ":true,"NA":true,"NAGOYA":false,"NAME":true,"NC":true,"NE":true,"NET":true,"NETWORK":true,"NEUSTAR":false,"NEW":false,"NEXUS":false,"NF":true,"NG":true,"NI":true,"NINJA":true,"NL":true,"NO":true,"NP":true,"NR":true,"NRA":false,"NRW":false,"NU":true,"NYC":false,"NZ":true,"OM":true,"ONG":false,"ONL":false,"OOO":false,"ORG":true,"ORGANIC":false,"OVH":true,"PA":true,"PARIS":false,"PARTNERS":false,"PARTS":false,"PE":true,"PF":true,"PG":true,"PH":true,"PHARMACY":false,"PHOTO":false,"PHOTOGRAPHY":false,"PHOTOS":false,"PICS":false,"PICTURES":false,"PINK":false,"PK":true,"PL":true,"PLACE":false,"PLUMBING":false,"PM":true,"PN":false,"POST":false,"PR":true,"PRAXI":false,"PRESS":false,"PRO":true,"PROD":true,"PRODUCTIONS":false,"PROPERTIES":false,"PS":true,"PT":true,"PUB":false,"PW":true,"PY":true,"QA":true,"QPON":false,"QUEBEC":false,"RE":true,"REALTOR":false,"RECIPES":false,"RED":true,"REISE":false,"REISEN":false,"RENTALS":false,"REPAIR":false,"REPORT":false,"REPUBLICAN":false,"REST":false,"RESTAURANT":false,"REVIEWS":false,"RICH":false,"RIO":false,"RO":true,"ROCKS":true,"RODEO":false,"RS":true,"RU":true,"RUHR":false,"RW":true,"SA":true,"SAARLAND":false,"SARL":false,"SB":true,"SC":true,"SCB":false,"SCOT":false,"SD":true,"SE":true,"SERVICES":false,"SEXY":false,"SG":true,"SH":true,"SHIKSHA":false,"SHOES":false,"SI":true,"SINGLES":false,"SK":true,"SL":true,"SM":true,"SN":true,"SO":true,"SOCIAL":false,"SOFTWARE":false,"SOLAR":false,"SOLUTIONS":true,"SOY":false,"SPACE":false,"SR":true,"ST":true,"SU":true,"SUPPLY":false,"SUPPORT":false,"SURF":false,"SV":true,"SX":true,"SY":true,"SYSTEMS":true,"SZ":true,"TATAR":false,"TATTOO":false,"TAX":false,"TC":true,"TD":false,"TECHNOLOGY":true,"TEL":false,"TF":true,"TG":true,"TH":true,"TIPS":false,"TIROL":false,"TJ":true,"TK":true,"TL":true,"TM":true,"TN":true,"TO":true,"TODAY":false,"TOKYO":false,"TOOLS":false,"TOWN":false,"TP":true,"TR":true,"TRADE":false,"TRAINING":false,"TRAVEL":true,"TT":true,"TV":true,"TW":true,"TZ":true,"UA":true,"UG":true,"UK":true,"UNIVERSITY":false,"UNKNOWN":true,"UNO":false,"US":true,"UY":true,"UZ":true,"VA":true,"VC":true,"VE":true,"VEGAS":false,"VENTURES":false,"VERSICHERUNG":false,"VG":true,"VI":true,"VILLAS":false,"VISION":false,"VLAANDEREN":false,"VN":true,"VODKA":false,"VOTE":false,"VOTING":false,"VOTO":false,"VOYAGE":false,"VU":true,"WANG":false,"WATCH":false,"WEBCAM":false,"WEBSITE":true,"WED":false,"WF":true,"WHOSWHO":false,"WIEN":false,"WIKI":false,"WILLIAMHILL":false,"WORK":false,"WORKS":false,"WORLD":false,"WS":true,"WTF":false,"XN--3DS443G":false,"XN--4GBRIM":false,"XN--55QX5D":false,"XN--6FRZ82G":false,"XN--80ADXHKS":false,"XN--80AO21A":false,"XN--80ASEHDB":false,"XN--80ASWG":false,"XN--C1AVG":false,"XN--D1ACJ3B":false,"XN--FIQ228C5HS":false,"XN--FIQS8S":false,"XN--FIQZ9S":false,"XN--I1B6B1A6A2E":false,"XN--J1AMH":false,"XN--J6W193G":false,"XN--KPRY57D":false,"XN--KPUT3I":false,"XN--MGBAAM7A8H":false,"XN--MGBERP4A5D4AR":false,"XN--NGBC5AZD":false,"XN--NQV7F":false,"XN--NQV7FS00EMA":false,"XN--P1ACF":false,"XN--P1AI":true,"XN--Q9JYB4C":false,"XN--RHQV96G":false,"XN--SES554G":false,"XXX":true,"XYZ":true,"YACHTS":false,"YANDEX":false,"YE":true,"YOKOHAMA":false,"YT":false,"ZA":true,"ZM":true,"ZONE":true,"ZW":true
};

function valid(node) {
	return node.nodeType == 1 &&
		(!re.excludingTag.test(node.nodeName) &&
		!re.excludingClass.test(node.className) ||
		re.includingClass.test(node.className)) &&
		node.contentEditable != "true" &&
		node.className.indexOf("linkifyplus") < 0;
}

function validRoot(node) {
	var cache = node;
	while (node.parentNode != document.documentElement) {
		// Check if the node is valid
		if (!valid(node) || node.LINKIFY_INVALID) {
			cache.LINKIFY_INVALID = true;
			return false;
		}
		// The node was detached from DOM tree
		if (!node.parentNode) {
			return false;
		}
		node = node.parentNode;
	}
	return true;
}

function isWbrOrText(node) {
	return node && (node.nodeType == 3 || node.nodeName == "WBR");
}

var traverser = {
	queue: [],
	running: false,
	start: function(){
		if (traverser.running) {
			return;
		}
		traverser.running = true;
		traverser.container();
	},
	container: function(){
		var root = traverser.queue.shift();

		if (!root) {
			traverser.running = false;
			return;
		}

		if (!root.childNodes || !root.childNodes.length || !validRoot(root)) {
			root.inTraverserQueue = false;
			setTimeout(traverser.container, 0);
			return;
		}

		var state = {
			currentNode: root.childNodes[0],
			lastMove: 1,	// 1=down, 2=left, 3=up
			loopCount: 0,
			timeStart: Date.now()
		};

		function traverse(){
			var i = 0, child, sibling, parent, range;

			while (state.currentNode != root) {
				// Create range to select textNode/wbr
				if (state.currentNode.nodeType == 3) {
					range = document.createRange();
					range.setStartBefore(state.currentNode);
					while (isWbrOrText(state.currentNode.nextSibling)) {
						state.currentNode = state.currentNode.nextSibling;
					}
					range.setEndAfter(state.currentNode);
				}

				// Cache next node for transfer
				child = state.currentNode.childNodes[0];
				sibling = state.currentNode.nextSibling;
				parent = state.currentNode.parentNode;

				if (range) {
					linkifyTextNode(range);
					range.detach();
					range = null;
				}

				// State transfer
				if (valid(state.currentNode) && state.lastMove != 3 && child) {
					state.currentNode = child;
					state.lastMove = 1;
				} else if (sibling) {
					state.currentNode = sibling;
					state.lastMove = 2;
				} else if (parent) {
					state.currentNode = parent;
					state.lastMove = 3;
				} else {
					break;
				}

				// Loop counter
				i++;
				state.loopCount++;
				if (i > 50) {
					setTimeout(traverse, 0);
					return;
				}
			}

			if (config.generateLog) {
				if (state.currentNode != root) {
					console.log("Traversal terminated! Last node: ", state.currentNode);
				} else {
					console.log(
						"Traversal end! Traversed %d nodes in %dms.",
						state.loopCount + 1,
						Date.now() - state.timeStart
					);
				}
			}

			root.inTraverserQueue = false;
			setTimeout(traverser.container);
		}
		setTimeout(traverse, 0);
	},
	add: function(node) {
		if (node.inTraverserQueue) {
			return;
		}
		node.inTraverserQueue = true;
		traverser.queue.push(node);
	}
};

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

	var excludingTag = [
		'a', 'code', 'head', 'noscript', 'option', 'script', 'style',
		'title', 'textarea', "svg", "canvas", "button", "select", "template",
		"meter", "progress", "math", "h1", "h2", "h3", "h4", "h5", "h6", "time"
	];

	var excludingClass = [
		"highlight", "editbox", "code", "brush:", "bdsug", "spreadsheetinfo"
	].concat(getArray(config.classBlackList));

	var includingClass = [
		"bbcode_code"
	].concat(getArray(config.classWhiteList));

	re = {
		excludingTag: new RegExp("^(" + excludingTag.join("|") + ")$", "i"),
		excludingClass: new RegExp("(^|\\s)(" + excludingClass.join("|") + ")($|\\s)"),
		includingClass: new RegExp("(^|\\s)(" + includingClass.join("|") + ")($|\\s)")
	};
}

function getArray(s) {
	s = s.trim();
	if (!s) {
		return [];
	}
	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] * 1 > 255 || (m[i].length > 1 && m[i][0] == "0")) {
			return false;
		}
	}
	return true;
}

function stripSingleParenthesis(str) {
	var i, pos, c = ")";
	for (i = 0; i < str.length; i++) {
		if (str[i] == "(") {
			if (c != ")") {
				return str.substring(0, pos);
			}
			pos = i;
			c = "(";
		}
		if (str[i] == ")") {
			if (c != "(") {
				return str.substring(0, i);
			}
			pos = i;
			c = ")";
		}
	}
	if (c != ")") {
		return str.substring(0, pos);
	}
	return str;
}

function getYoutubeId(url) {
	var match =
		url.match(/https?:\/\/www\.youtube\.com\/watch\?v=([^&]+)/) ||
		url.match(/https?:\/\/youtu\.be\/([^?]+)/);
	return match && match[1];
}

function createLink(url, text) {
	var cont;

	cont = document.createElement("a");
	cont.href = url;
	if (config.openInNewTab) {
		cont.target = "_blank";
	}
	cont.appendChild(text);
	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));
		}
	}
	subRange.detach();

	// 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, linkified = false;

	while (m = urlRE.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) {
				urlRE.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].toUpperCase() in tlds)) {
			continue;
		}

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

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

			// Get the link without single parenthesis
			face = stripSingleParenthesis(face);
			path = stripSingleParenthesis(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;

		urlRE.lastIndex = m.index + face.length;
		lastIndex = urlRE.lastIndex;

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

		linkified = true;
	}

	if (!linkified) {
		return;
	}

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

	replaceRange(range, nodes);
}

function template(text, option) {
	var key;
	for (key in option) {
		text = text.split("@" + key).join(option[key]);
	}
	return text;
}

function observeDocument(callback) {

	callback(document.body);

	new MutationObserver(function(mutations){
		var i;
		for (i = 0; i < mutations.length; i++) {
			if (!mutations[i].addedNodes.length) {
				continue;
			}
			callback(mutations[i].target);
		}
	}).observe(document.body, {
		childList: true,
		subtree: true
	});
}

function loop(list, callback) {
	var i = 0,
		len = list.length;

	function chunk() {
		var j;
		for (j = 0; j < 50 && i < len; i++, j++) {
			callback(list[i]);
		}
		if (i < len) {
			setTimeout(chunk);
		}
	}

	setTimeout(chunk);
}

var embedFunction = {
	image: function(url, element) {
		var obj;
		if (!config.useImg || !/^[^?#]+\.(jpg|png|gif|jpeg)($|[?#])/i.test(url)) {
			return null;
		}
		obj = document.createElement("img");
		obj.className = "embedme-image";
		obj.alt = url;
		obj.src = url;
		element = element.cloneNode(false);
		element.appendChild(obj);
		return element;
	},
	youtube: function(url) {
		var id, cont, wrap, obj;
		if (!config.useYT || !(id = getYoutubeId(url))) {
			return null;
		}
		cont = document.createElement("div");
		cont.className = "embedme-video";

		wrap = document.createElement("div");
		wrap.className = "embedme-video-wrap";
		cont.appendChild(wrap);

		obj = document.createElement("iframe");
		obj.className = "embedme-video-iframe";
		obj.src = "https://www.youtube.com/embed/" + id;
		obj.setAttribute("allowfullscreen", "true");
		obj.setAttribute("frameborder", "0");
		wrap.appendChild(obj);

		return cont;
	}
};

function embedContent(element) {
	var url = element.href, key, embed;

	if (!element.parentNode) {
		return;
	}

	for (key in embedFunction) {
		embed = embedFunction[key](url, element);
		if (embed) {
			embed.classList.add("embedme");
			element.parentNode.replaceChild(embed, element);
			return;
		}
	}
//	element.classList.add("embedme-fail");
}

function embedMe(node) {
	var result, nodes = [], i, xpath;

	if (config.embedAll) {
		xpath = ".//a[not(*) and text() and @href and not(contains(@class, 'embedme'))]";
	} else {
		xpath = ".//a[not(*) and text() and @href and not(contains(@class, 'embedme')) and contains(@class, 'linkifyplus')]";
	}

	result = document.evaluate(xpath, node, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);

	for (i = 0; i < result.snapshotLength; i++) {
		nodes.push(result.snapshotItem(i));
	}

	loop(nodes, embedContent);
}

GM_addStyle(template(".embedme-image{max-width:100%}.embedme-video{max-width:@ytWidthpx}.embedme-video-wrap{position:relative;padding-top:30px;padding-bottom:@ytRatio%}.embedme-video-iframe{position:absolute;top:0;left:0;width:100%;height:100%}", {
	ytWidth: config.ytWidth,
	ytRatio: (config.ytHeight / config.ytWidth) * 100
}));

GM_registerMenuCommand("Linkify Plus Plus - Configure", function(){
	GM_config.open();
});

observeDocument(function(node){
	traverser.add(node);
	traverser.start();
});

observeDocument(embedMe);