Danbooru - Miscellaneous Tweaks

By Mango Last update May 12, 2013 — Installed 38,393 times.

There are 150 previous versions of this script.

Add Syntax Highlighting (this will take a few seconds, probably freezing your browser while it works)

// ==UserScript==
// @name           Danbooru - Miscellaneous Tweaks
// @namespace      http://userscripts.org/scripts/show/27776
// @description    Adds a variety of useful tweaks to Danbooru, each of which can be easily disabled.
// @include        http://danbooru.donmai.us/*
// @include        https://danbooru.donmai.us/*
// @include        http://hijiribe.donmai.us/*
// @include        http://sonohara.donmai.us/*
// @include        http://www.donmai.us/*
// @include        http://donmai.us/*
// @grant          GM_getValue
// @grant          GM_setValue
// @grant          GM_listValues
// @grant          GM_deleteValue
// @grant          GM_xmlhttpRequest
// @grant          GM_log
// @grant          GM_openInTab
// @version        2013.05.12
// ==/UserScript==

/*
	Settings are modified by clicking the "Tweaks" link at the right end of the navbar on most pages.
	You do not need to modify the script itself.
*/

var showError, recursion, login, loginID, content, navbar, subnavbar;
initialize();
loadTweaks();

function loadTweaks()
{
	//Returns an array with just the "value" properties from an array of arguments for a tweak
	function defaultArgs( tweak )
	{
		var defArgs = [];
		for( var i = 0; i < tweak.args.length; i++ )
			defArgs.push( tweak.args[i].value );
		return defArgs;
	}
	
	//Validates the regex properties of an array of objects
	function validRegexInArray(regexName){ return function(elem, args, argI){
			var temp = args[argI], json = validJSON(elem, args, argI);
			if( json !== false && (json instanceof Array) ) try {
				for( var i = 0; i < json.length; i++ )
					(new RegExp( "("+json[i][regexName]+")" )).test("blah");
				
				return (args[argI] = json);
			}
			catch(err) { }
			elem.style.color = "red";
			args[argI] = temp;
			return false;
	}}
		
	//Validates regex argument
	function validRegex(elem, args, argI)
	{
		try{
			var ex = new RegExp("("+elem.value+")");
			ex.test("blah");
			args[argI] = elem.value;
			elem.style.removeProperty('color');
			return elem.value;
		}catch(err){
			elem.style.color = 'red';
			return false;
		}
	}
	
	//Validates number argument
	function validNumber(a,b,x)
	{
		var temp = function(elem, args, argI)
		{
			var num = Number( elem.value );
			if( isNaN(num) || ( a !== undefined && num < a ) )
				args[argI] = a;
			else if( b > a && num > b )
				args[argI] = b;
			else
			{
				args[argI] = num;
				elem.style.removeProperty('color');
				return num;
			}
			elem.style.color = 'red';
			return false;
		}
		
		if( x === undefined )
			return temp;//Called with < 3 parameters.  We just want the function
		
		//Three params.  We're setting a value without a range.
		temp = validNumber();
		return temp(a,b,x);
	};
	
	//Validates JSON argument
	function validJSON(elem, args, argI)
	{
		try{
			var value = MS_parseJSON( elem.value );
			args[argI] = value;
			elem.style.removeProperty("color");
			return value;
		}catch(err){}
		elem.style.color = "red";
		return false;
	}
	
	function validTagSubArray(elem, args, argI)
	{
		try{
			var query = -1, call, json = MS_parseJSON( elem.value );
			
			for( call = 0; call < json.length && query < 0 &&
					json[call].title.length > 0 && // Non-empty title
					json[call].refresh > 0 && // Refresh interval greater than zero
					json[call].thumbs > 0 && // At least one thumbnail to display
					json[call].queries.length > 0; call++ ) // At least one query
			{
				for( query = json[call].queries.length - 1; query >= 0 &&
						json[call].queries[query].search.length > 0 && // Non-empty tags
						json[call].queries[query].pages > 0 && // At least one page
						json[call].queries[query].pages <= 1000 && // No more than 1000(!) pages
						( !json[call].queries[query].filter || json[call].queries[query].filter instanceof Array ); query-- ); // Filter is an array if specified
			}
			if( query < 0 && call > 0 && call == json.length )
			{
				args[argI] = json;
				elem.style.removeProperty("color");
				return json;
			}
		}
		catch(err){}
		
		elem.style.color = "red";
		return false;
	}
	
	function validSHA1(elem, args, argI)
	{
		if( /^[a-f0-9]{40}$/i.test( elem.value ) )
		{
			args[argI] = elem.value;
			elem.style.removeProperty("color");
			return elem.value;
		}
		
		elem.style.color = "red";
		return false;
	}
	
	//Creates the input element for an element and sets up the appropriate settings/listeners
	function setupArg( oldSettings, tweak, argI, elem )
	{
		//Let the starting value be the custom setting, or default if nothing set
		var currentValue = tweak.args[argI].value;
		if( oldSettings[ tweak.name ] && oldSettings[ tweak.name ].args && oldSettings[ tweak.name ].args[argI] != null )
			currentValue = oldSettings[ tweak.name ].args[argI];
		
		if( typeof( tweak.args[argI].value ) == "boolean" )
		{
			//Checkbox
			elem = elem.appendChild( createElementX({ tag:"input", type:"checkbox", title:tweak.args[argI].desc }) );
			elem.checked = currentValue;
			tweak.args[argI].set = function(reset)
			{
				var settings = MS_getValue( "settings" );
				if( !settings[ tweak.name ].args )
				{
					//Populate custom settings with the default arguments
					settings[ tweak.name ].args = defaultArgs( tweak );
				}
				
				if( reset === true )
				{
					//Set to default value and delete the custom settings
					elem.checked = tweak.args[argI].value;
					settings[ tweak.name ] = { enable:tweak.enable, version:tweak.version };
				}
				else settings[ tweak.name ].args[argI] = elem.checked;
				MS_setValue( "settings", settings, -1 );
			};
			elem.addEventListener( "click", tweak.args[argI].set, false );
		}
		else if( typeof( tweak.args[argI].value ) == "number" )
		{
			if( !tweak.args[argI].setValid )
				tweak.args[argI].setValid = validNumber;
			
			elem = elem.appendChild( createElementX({ tag:"input", type:"text", title:tweak.args[argI].desc }) );
			elem.value = currentValue;
			
			tweak.args[argI].set = function(reset)
			{
				var value, settings = MS_getValue( "settings" );
				
				if( !settings[ tweak.name ].args )
					settings[ tweak.name ].args = defaultArgs( tweak );
				if( reset === true )
				{
					settings[ tweak.name ] = { enable:tweak.enable, version:tweak.version };
					elem.value = tweak.args[argI].value;
					elem.style.removeProperty("color");//Assume default arguments are valid
				}
				else tweak.args[argI].setValid( elem, settings[ tweak.name ].args, argI );
				
				MS_setValue( "settings", settings, -1 );
			};
			elem.addEventListener( "input", tweak.args[argI].set, false );
		}
		else if( typeof( tweak.args[argI].value ) == "string" )
		{
			if( tweak.args[argI].elem == "input" )
			{
				elem = elem.appendChild( createElementX({ tag:"input", type:"text", title:tweak.args[argI].desc }) );
				
				//Size element to fit the contents
				elem.addEventListener( "input", function() { elem.setAttribute("size", elem.value.length + 5); }, false );
				elem.setAttribute("size", currentValue.length + 5);
			}
			else
			{
				//Size to fit, with a timeout since hidden elements don't have a height
				elem = elem.appendChild( createElementX({ tag:"textarea", type:"text", style:"display: inline-block; width:95%; height:20px", title:tweak.args[argI].desc }) );
				setTimeout( function() { elem.style.height = Math.max( elem.scrollHeight, elem.clientHeight, 20 ) + "px"; }, 0 );
				elem.addEventListener( "input", function() { elem.style.height = Math.max( elem.scrollHeight, elem.clientHeight ) + "px"; }, false );
			}
			elem.value = currentValue;
			
			tweak.args[argI].set = function(reset)
			{
				var settings = MS_getValue( "settings" );
				
				if( !settings[ tweak.name ].args )
					settings[ tweak.name ].args = defaultArgs( tweak );
				if( reset === true )
				{
					settings[ tweak.name ] = { enable:tweak.enable, version:tweak.version };
					elem.value = tweak.args[argI].value;
					elem.style.removeProperty("color");
				}
				else if( tweak.args[argI].setValid )
					tweak.args[argI].setValid( elem, settings[ tweak.name ].args, argI );
				else
					settings[ tweak.name ].args[argI] = elem.value;//No validation function, assume valid
					
				MS_setValue( "settings", settings, -1 );
			}
			elem.addEventListener( "input", tweak.args[argI].set, false );
		}
		else if( typeof( tweak.args[argI].value ) == "object" )
		{
			if( !tweak.args[argI].setValid )
				tweak.args[argI].setValid = validJSON;
			
			if( tweak.args[argI].desc.length )
				elem.appendChild( createElementX({tag:"br"}) );
			elem = elem.appendChild( createElementX({ tag:"textarea", type:"text", style:"display: inline-block; width:95%; height:20px", title:tweak.args[argI].desc }) );
			setTimeout( function() { elem.style.height = Math.max( elem.scrollHeight, elem.clientHeight, 20 ) + "px"; }, 0 );
			
			//Insert spaces to help with linebreaking
			elem.value = JSON.stringify( currentValue, null, 1 ).replace(/\s+/g,' ');
			
			tweak.args[argI].set = function(reset)
			{
				var settings = MS_getValue( "settings" );
				if( !settings[ tweak.name ].args )
					settings[ tweak.name ].args = defaultArgs( tweak );
				if( reset === true )
				{
					settings[ tweak.name ] = { enable:tweak.enable, version:tweak.version };
					elem.value = JSON.stringify( tweak.args[argI].value, null, 1 ).replace(/\s+/g,' ');
					elem.style.removeProperty("color");
				}
				else tweak.args[argI].setValid( elem, settings[ tweak.name ].args, argI );
				
				elem.style.height = Math.max( elem.scrollHeight, elem.clientHeight) + "px";
				MS_setValue( "settings", settings, -1 );
			}
			elem.addEventListener( "input", tweak.args[argI].set, false );
		}
		else tweak.args[argI].set = function(){ GM_log("No setter for type "+typeof( tweak.args[argI].value )+" ("+tweak.name+")"); }
	}
	
	var defaultAddStyle =
		'/* Hide the Danbooru header */\n'+
		'header h1 { display: none }\n'+
		'\n'+
		'/* Shove the forum post IDs listed on the Reply rows to the right and color them white. */\n'+
		'div.list-of-forum-posts menu li:first-child { float:right; color:white }\n'+
		'\n'+
		'/* Hide the post search Go button */\n'+
		'*#search-box form input[type="submit"] { display: none }\n'+
		'\n'+
		'/* Increase the space above the navbar */\n'+
		'menu.main { margin-top: 15px }\n'+
		'\n'+
		'/* Reduce height between thumbnail blocks on profile pages */\n'+
		'div#c-users div#a-show div.box { margin-bottom: 0em; }\n'+
		'\n'+
		'/* Make font size more uniform */\n'+
		'body { font-size: 80% }\n'+
		'textarea { font-size: 1.2em }\n'+
		'\n'+
		'/* Increase the space around thumbnails */\n'+
		'article.post-preview { min-height: 175px; min-width: 165px; }\n'+
		'.preview { min-height: 175px; min-width: 160px; }\n'+
		'\n'+
		'/* Put a little space between the thumbnails and their status borders. */\n'+
		'.post-preview img { padding:2px }\n'+
		'\n'+
		'/* Shrink the comment width a tiny bit to accommodate the above width changes */\n'+
		'.list-of-comments { width: 98% }\n'+
		'\n'+
		'/* Increase space between the Popular navigators */\n'+
		'div#c-explore-posts .period { margin: 0px 5em; }\n'+
		'\n'+
		'/* Reduce padding around post status notices */\n'+
		'div#c-posts div#a-show #pool-nav, div#c-posts div#a-show #search-seq-nav, div#c-posts div#a-show #nav-help { margin: 0px 0px 0.5em 0px; }\n'+
		'div#c-posts div.nav-notice { padding: 0.5em; }\n'+
		'div#c-posts div.notice { padding: 0.5em; margin-bottom: 0.5em; }\n'+
		'\n'+
		'/* Hide upload guide notice */\n'+
		'div#upload-guide-notice { display:none }\n';
	
	var tweaks =
	[
		{ name:"Metasettings", enable:false, desc:"Settings affecting the script's operation as a whole.", func:metaSettings, version:2, args:[{ desc:"Check for script updates once a day (requires GM_xmlhttpRequest)", value:true }, { desc:"Display alerts if tweaks fail", value:false } ] },
		
		{ name:"Add Tag Subscription", enable:false, desc:"Adds pseudo tag subscriptions to your user page.<br><u>title</u>: Title of the subscription, linked to the first query.  The title must be unique.<br><u>timestamp</u>: Appends a timestamp of the last refresh to the title.<br><u>refresh</u>: Hours to wait before refreshing the subscription.<br><u>thumbs</u>: Number of thumbnails to display.<br><u>intersect</u>: Return posts matching ALL queries (using the order of the last) rather than ANY (sorted by post ID).<br><u>queries</u>: List of objects representing post searches.<br><u>search</u>: Tags to search for; the usual limitations apply.<br><u>pages</u>: Number of pages to collect for this query, each page containing 100 posts.<br><u>filter</u>: Drop any posts from this query's results that don't also contain all of the tags in the filter.  Metatags are not supported.", func:addTagSubscription, version:1, args:[ { desc:"", value:[ { title:"Recently Translated", timestamp:false, refresh:1, thumbs:6, intersect:true, queries:[ { search:"order:note user:"+login, pages:1, filter:[] } ] }, { title:"Recently Commented", timestamp:false, refresh:1, thumbs:6, intersect:true, queries:[ { search:"order:comment user:"+login, pages:1, filter:[] } ] } ], setValid:validTagSubArray } ] },
		
		{ name:"Arrow Key Navigation", enable:false, desc:"Allows navigation between pages using the arrow keys.", func:arrowKeyNavigation, version:0, args:[] },
		
		{ name:"Blacklist", enable:false, desc:"Blacklist implementation that replaces images (full and thumbnail) with links labeled with the tags they matched in the blacklist.  Clicking the link/image alternates between the two.  Translation notes appear every other time the image is displayed.  User and rating metatags are supported.  Your own posts are exempt from the blacklist.", func:blacklistTags, version:0, args:[{ desc:"Input to regex constructor [/^(xxx)$/i]", value:"blockedTag1|blockedTag2|blockedTag3", setValid:validRegex }] },
				
		{ name:"Change +/- Links", enable:false, desc:"Adds +/- tag links when the taglist doesn't have them.", func:changePlusMinusLinks, version:1, args:[{ desc:"Make links add/remove tags from search box instead of launching new searches", value:true }] },
		
		{ name:"Custom Taglist", enable:false, desc:"Adds a new taglist section below tag lists, complete with +/- links.  Tag types are supported.", func:customTaglist, version:0, args:[{ desc:"Title of list", value:"Custom Tags", elem:"input" }, { desc:"Tags", value:[ 'art:bomber_grape', 'char:maribel_hearn', 'copy:hidamari_sketch', 'order:score', 'order:comment', 'comm:'+login, 'rating:e', 'rating:q', 'rating:s', 'status:any', 'fav:'+login, 'user:'+login, 'source:pixiv/*', 'arttags:0', 'chartags:1', 'copytags:1', 'gentags:1' ] }] },
		
		{ name:"Double Down", enable:false, desc:"Requires down-vote links on posts and comments to be clicked twice in a row to count.", func:doubleDown, version:0, args:[ { desc:"Color to apply to down links the first time they are clicked", value:"red", elem:"input" } ] },
		
		{ name:"Hide User Statistics", enable:false, desc:"", func:hideUserStatistics, version:0, args:[{ desc:"Stats to remove from your profile", value:[ "Statistics", "Inviter", "Approvals" ] }, { desc:"Stats to remove from others' profiles", value:[ "Inviter" ] }] },
				
		{ name:"Navbar Links", enable:false, desc:"Adds links to the left side of the navbar.", func:navbarLinks, version:0, args:[{ desc:"", value:[{ text:"Upload", href:"/uploads/new" }, { text:"IQDB", href:"http://danbooru.iqdb.org/" }] }] },
		
		{ name:"Navbar Tag Search", enable:false, desc:"Adds/moves the post search box to the navbar.", func:navbarTagSearch, version:1, args:[{ desc:"Move the post search box when it exists", value:true }, { desc:"Add to subnavbar instead of navbar", value:true }] },
		
		{ name:"Precision Time", enable:false, desc:"Sets all time fields to the 'X units ago' form.", func:precisionTime, version:0, args:[ { desc:"Precision", value:1, setValid:validNumber(0,6) } ] },
		
		{ name:"Score Thumbnails", enable:false, desc:"Displays scores and favorite counts below thumbnails.", func:scoreThumbnails, version:0, args:[ { desc:"Show score", value:true }, { desc:"Minutes to keep favcount in cache (0 = no favcount)", value:0, setValid:validNumber(0) } ] },

		{ name:"Source Stat Links", enable:false, desc:"Running artist/source searches for posts without artist tags and displays the results under the source information in the sidebar.", func:sourceStatLinks, version:0, args:[{ desc:"Run \"Find Artist\" search", value:true }, { desc:"Run post search", value:false }] },
		
		{ name:"Style User Levels", enable:false, desc:"Applies styles to links to user pages depending on their level.", func:styleUserLevels, version:1, args:[ { desc:"Days to cache each user level", value:5, setValid:validNumber(1,90) }, { desc:"Styles", value:{ Admin: 'color:red', Mod: 'color:orange', Janitor: 'color:green', Contributor: 'color:purple', Builder: 'color:#6633FF', Platinum: 'color:#0000FF', Gold: 'color:#0000FF', Member: '', Blocked: 'color:black', Unknown:"color:gray"} } ] },
		
		{ name:"Add Style", enable:false, desc:"Adds CSS styles to all pages.  The 'important' flag is added automatically.  NOTE: This tweak is the most likely to reset after script updates, so backup any changes you make.", func:MS_addStyle, version:6, args:[{ desc:"", value:defaultAddStyle }] }
	];
	
	var i, settings = MS_getValue( "settings", {} ), changed = false;
	
	for( i = 0; i < tweaks.length; i++ )
	{
		if( !settings[ tweaks[i].name ] || settings[ tweaks[i].name ].version != tweaks[i].version )
		{
			settings[ tweaks[i].name ] = { enable:tweaks[i].enable, version:tweaks[i].version };
			changed = true;
		}
		
		try{
			if( settings[ tweaks[i].name ].args )
			{
				//Custom settings exist.  Run tweak if enabled
				if( settings[ tweaks[i].name ].enable )
					tweaks[i].func.apply( this, settings[ tweaks[i].name ].args );
			}
			else if( tweaks[i].enable )
			{
				//No custom settings, run with default arguments if enabled by default
				tweaks[i].func.apply( this, defaultArgs( tweaks[i] ) );
			}
		}
		catch(e) { if( showError ) MS_alert("Error ("+tweaks[i].name+"): "+e); }
	}
	
	if( changed )
		MS_setValue( "settings", settings, -1 );
	
	content = !recursion && navbar && document.getElementById("page");
	if( content )
	{
		var settingsLink = navbar.appendChild( createElementX({ tag: "li" }) ).appendChild( createElementX({ tag: "a", text: "Tweaks" }) );
		var settingsDiv = content.parentNode.insertBefore( createElementX({ tag:"div", style:"display:none; padding:0 20px 30px 20px;", id:"tweaks_settings" }), content );
		
		settingsLink.addEventListener( "mousedown", function()
		{
			if( content.style.display == "none" )
			{
				//Closing settings.  Destroy the contents and unhide the original content.
				settingsDiv.style.display = "none";
				content.style.display = "block";
				settingsDiv.textContent = "";
			}
			else
			{
				//Opening settings.  Hide the current content and reconstruct the GUI from scratch.
				var settings = MS_getValue( "settings", {} );
				
				settingsDiv.appendChild( createElementX({ tag:"h4" }) ).appendChild( createElementX({ tag:"a", href:"http://userscripts.org/scripts/show/27776", text:"Miscellaneous Tweaks", style:"color:black;" }, " settings") );
				settingsDiv.appendChild( createElementX( { tag:"hr" }, "Questions?  Comments?   Problems?  Leave some feedback at ", { tag:"a", href:"http://userscripts.org/scripts/discuss/27776", text:"http://userscripts.org/scripts/discuss/27776" }, "!", { tag:"hr" }) );
				var table = settingsDiv.appendChild( createElementX({ tag:"table", width:"100%", style:"margin-bottom: 2em; vertical-align:top;" }) );
				
				var resetAll = false, resetButtons = [];
				
				//Clear old unused settings
				for( sName in settings )
				{
					for( var i = 0; i < tweaks.length && sName != tweaks[i].name; i++ );
					if( i == tweaks.length )
					{
						GM_log("Deleting old setting: "+sName);
						delete settings[sName];
						MS_setValue( "settings", settings, -1 );
					}
				}
				
				for( var i = 0; i < tweaks.length; i++ )
				{
					var reset, body, check, title, tr = table.appendChild( createElementX({ tag:"tr", style:"vertical-align:top;" }) );
					var border = i != tweaks.length - 1 ? "border-bottom:4px solid #EEE; " : "";
					
					//Button to reset to default settings
					reset = tr.appendChild( createElementX({ tag:"td", style:border+"padding:1px 4px 1em 4px; width:1%" }) ).appendChild( createElementX({ tag:"button", title:"Reset to default settings for this tweak.", text:"Reset" }) );
					resetButtons.push( reset );//pad 1 4
					
					//Checkbox to toggle tweak
					check = tr.appendChild( createElementX({ tag:"td", style:border+"padding:0.3em 4px 1em 4px; text-align:center" }) ).appendChild( createElementX({ tag:"input", type:"checkbox", title:"Toggle this tweak." }) );//pad 6 4
					check.checked = ( settings[ tweaks[i].name ].args ? !!settings[ tweaks[i].name ].enable : !!tweaks[i].enable );
					
					//Tweak description and arguments
					body = tr.appendChild( createElementX({ tag:"td", style:border+"padding:1px 4px 1em 4px; " }) );//pad 1 4 10 4
					title = body.appendChild( createElementX({ tag:"b", text:tweaks[i].name }) );
					title.style.setProperty( "text-decoration", check.checked ? "none" : "line-through", null );
					
					//body.appendChild( createElementX(": "+tweaks[i].desc) );
					body.appendChild( createElementX(": ") );
					body.appendChild( createElementX({ tag:"span" }) ).innerHTML = tweaks[i].desc;
					
					//When the Reset button is clicked, delete any custom settings for it and fall back on defaults.
					reset.addEventListener( "click", (function(tweak,check){ return function()
					{
						if( !resetAll && !confirm("Reset this tweak?") )
							return;
						if( !!check.checked != !!tweak.enable )
							check.click();
						for( var i = 0; i < tweak.args.length; i++ )
							tweak.args[i].set(true);
					}; })( tweaks[i], check ), false );
					
					//When checkbox is clicked, toggle the title strikethrough and save settings
					check.addEventListener( "click", (function(tweak,title){ return function()
					{
						var settings = MS_getValue( "settings" );
						
						title.style.setProperty( "text-decoration", (settings[ tweak.name ].enable = this.checked) ? "none" : "line-through", null );
						if( !settings[tweak.name].args )
							settings[tweak.name].args = defaultArgs( tweak );
						MS_setValue( "settings", settings, -1 );
					}; })(tweaks[i],title), false );
					
					//If the tweak takes arguments, create the appropriate input elements and listeners.
					if( tweaks[i].args.length )
					{
						var list = body.appendChild( createElementX({ tag:"ul", style:"margin:0 0 0 2em;  list-style:disc" }) );
						for( var j = 0; j < tweaks[i].args.length; j++ )
							setupArg( settings, tweaks[i], j, list.appendChild( createElementX({ tag:"li", text:tweaks[i].args[j].desc+(tweaks[i].args[j].desc.length ? ": " : ""), style:"margin-top:4px" }) ) );
					}
				}
				settingsDiv.appendChild( createElementX({ tag:"hr" }) );
				
				//"Export" button
				settingsDiv.appendChild( createElementX({ tag:"input", type:"button", value:"Export", title:"Save settings to file.  There is no import option." }) ).addEventListener( "click", function()
				{
					try{ window.location ="data:application/octet-stream,"+escape( JSON.stringify( MS_getValue( "settings" ), null, 1 ).replace(/\s+/g,' ') ); }
					catch(err){}
				}, false );
				
				//"Reset All Tweaks" button
				var resetAllButton = settingsDiv.appendChild( createElementX({ tag:"input", type:"button", value:"Reset All Tweaks", title:"Reset all settings." }) ).addEventListener( "click", function()
				{
					if( confirm("Reset all tweaks?") )
					{
						resetAll = true;
						for( var i = 0; i < resetButtons.length; i++ )
							resetButtons[i].click();
						resetAll = false;
					}
				}, false );
				
				//"Delete All Variables" button
				var resetAllButton = settingsDiv.appendChild( createElementX({ tag:"input", type:"button", value:"Delete All Variables", title:"Reset all settings." }) ).addEventListener( "click", function()
				{
					if( confirm("WARNING: This will delete all variables associated with this script.") && confirm("...Really?") )
					{
						var varList = GM_listValues();
						while( varList.length > 0 )
							GM_deleteValue( varList.pop() );
						settingsLink.click();
					}
				}, false );
				
				content.style.display = "none";
				settingsDiv.style.display = "block";
			}
		}, false );
	}
}

function initialize()
{
	if( typeof(unsafeWindow) == "undefined" )
		var unsafeWindow=window;
	if( typeof(GM_getValue) == "undefined" || GM_getValue('a', 'b') == undefined )
	{
		GM_log = console.log || function() { };
		
		//http://userscripts.org/topics/41177
		GM_deleteValue = function(a){ localStorage.removeItem("danbooru_miscellaneous."+a); }
		GM_openInTab = function(a){ return window.open(a,a); }
		GM_getValue = function(name, defaultValue)
		{
			var value = localStorage.getItem("danbooru_miscellaneous."+name);
			if( !value )
				return defaultValue;
			
			var type = value[0];
			value = value.substring(1);
			
			if( type == 'b' )
				return value == 'true';
			else if( type == 'n' )
				return Number(value);
			return value;
		}
		GM_setValue = function(name, value)
		{
			value = (typeof value)[0] + value;
			localStorage.setItem("danbooru_miscellaneous."+name, value);
		}
		GM_listValues = function()
		{
			var i, j = 0, list = new Array(localStorage.length);
			for( i = 0; i < localStorage.length; i++ )
				if( /^danbooru_miscellaneous/.test( localStorage.key(i) ) )
					list[j++] = localStorage.key(i).replace(/^danbooru_miscellaneous./,'');
			return list;
		}
		GM_xmlhttpRequest = function(obj)
		{
			//Unlike the Greasemonkey function, XMLHttpRequest can't access sites outside Danbooru
			if( !/^https?:..[^.]+.donmai.us\//.test(obj.url) )
				return;
			
			var request = new XMLHttpRequest();
			request.onreadystatechange = function()
			{
				if( obj.onreadystatechange )
					obj.onreadystatechange( request );
				if( request.readyState == 4 && obj.onload )
					obj.onload( request );
			}
			request.onerror = function()
			{
				if( obj.onerror )
					obj.onerror( request );
			}
			try {
				request.open( obj.method, obj.url, true );
			} catch(e) {
				if( obj.onerror )
					obj.onerror( { readyState:4, responseHeaders:'', responseText:'', responseXML:'', status:403, statusText:'Forbidden'} ); 
				return;
			}
			if( obj.headers )
				for( name in obj.headers )
					request.setRequestHeader( name, obj.headers[name] );
			
			request.send( obj.data );
			return request;
		}
	}
	
	showError = false;
 	recursion = (window != window.top);
	
	loginID = document.getElementsByName("current-user-id");
	if( loginID.length == 0 )
	{
		login = "";
		loginID = 0;
	}
	else
	{
		login = document.getElementsByName("current-user-name")[0].content;
		loginID = parseInt(loginID[0].content);
	}
	
	//pass_hash = document.cookie.search("cookie_password_hash=(.+?)(;|$)") < 0 ? "" : document.cookie.match("cookie_password_hash=(.+?)(;|$)")[1];
	
	navbar = document.getElementById("site-map-link");
	if( navbar )
	{
		content = !recursion && document.getElementById("page");
		navbar = navbar.parentNode.parentNode;
		for( subnavbar = navbar.nextSibling; subnavbar && !subnavbar.tagName; subnavbar = subnavbar.nextSibling );
		if( !subnavbar || subnavbar.tagName != "MENU" )
		{
			//Insert subnavbar if it doesn't exist but the navbar does.
			subnavbar = navbar.parentNode.insertBefore( createElementX({tag:"menu"}), navbar.nextSibling );
			
			//Add a dummy link element so the subnavbar is the right height.
			subnavbar.appendChild( createElementX({tag:"li"}) ).appendChild( createElementX({tag:"a", href:"#"}) );
		}
	}
	
	if( typeof(custom) != "undefined" )
		custom();
}


function metaSettings(aUpdateCheck, aShowError)
{
	if( aUpdateCheck )
		updateCheck();
	showError = aShowError;
}


/**
	Checks for script updates.
 */
function updateCheck()
{
	var scriptNum = 27776;
	
	//Only check for update if using Greasemonkey and no check has been made in the last day.
	if( !MS_getValue('checked_update') )
	{
		MS_setValue( 'checked_update', true, 24*3600 );
		GM_xmlhttpRequest(
		{
			method: 'GET',
			url: 'http://userscripts.org/scripts/source/'+scriptNum+'.meta.js?'+new Date().getTime(),
			headers: { 'Cache-Control': 'no-cache' },
			onload: function(response)
			{
				var localVersion = parseInt( GM_getValue( 'local_version', 0 ) );
				var remoteVersion = parseInt( /@uso:version\s*([0-9]+?)\s*$/m.exec(response.responseText)[1] );
				
				if( !localVersion || remoteVersion <= localVersion )
					GM_setValue( 'local_version', remoteVersion );
				else if( confirm( 'There is an update available for the Greasemonkey script "'+/@name\s*(.*?)\s*$/m.exec(response.responseText)[1]+'".\nWould you like to go to the install page now?' ) )
				{
					GM_openInTab( 'http://userscripts.org/scripts/show/'+scriptNum );
					GM_setValue( 'local_version', remoteVersion );
				}
			},
		});
	}
}


/**
	Adds/moves the post search box to the navbar.
 */
function navbarTagSearch(moveBox,useSubnavbar)
{
	var targetBar = useSubnavbar ? subnavbar : navbar;
	if( recursion || !targetBar )
		return;
	
	var searchBox = document.getElementById("search-box"), value = "";
	if( searchBox )
	{
		if( !moveBox )
			return;
		
		if( document.getElementById("tags") )
			value = document.getElementById("tags").value.replace(/"/g,'');
		
		//To keep a consistent location for the search box, remove the original one wherever it appears.
		searchBox.parentNode.removeChild(searchBox);
	}

	searchBox = createElementX({ tag:"li", id:"search-box"});
	searchBox.innerHTML = '<form accept-charset="UTF-8" action="/posts" method="get"><div style="margin:0;padding:0;display:inline"><input name="utf8" type="hidden" value="&#x2713;" /></div><input id="tags" name="tags" size="30" type="text" ' + ( value ? 'value="'+value+'" ' : 'placeholder="Search posts" ' ) + '/><input name="commit" type="submit" value="Go" style="display:none" /></form>';
	
	targetBar.insertBefore( searchBox, targetBar.firstChild );
}


/**
	Causes the +/- links next to tags in the taglist (which appear for normal tags for Privileged+ users)
	to add/remove/negate tags from the tag search box. Tags cannot be added or negated more than once.
 */
function changePlusMinusLinks(addListeners)
{
	if( recursion || !document.getElementById("tags") )
		return;

	setTimeout( function() //0-timeout to let the custom tags get added
	{
		var boxValue = document.getElementById("tags").value;
		if( boxValue.length > 0 )
			boxValue = '+'+boxValue;
		
		var tagList = document.evaluate( "//section/ul/li/a[@class='wiki-link' and text() = '?']/..", document, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null );
				
		for( var i = 0; i < tagList.snapshotLength; i++ )
		{
			var links = tagList.snapshotItem(i).getElementsByTagName("a");
			if( links.length < 4 )
			{
				var urlTag = links[0].href.replace( /.*title=/i, '' );
				tagList.snapshotItem(i).insertBefore( createElementX(
					" ", { tag:'a', text:'+', class:"search-inc-tag", href:"/posts?tags=" +urlTag+boxValue },
					" ", { tag:'a', text:'-', class:"search-exl-tag", href:"/posts?tags=-"+urlTag+boxValue }), links[0].nextSibling );
				
				links = tagList.snapshotItem(i).getElementsByTagName("a");				
				links[2].innerHTML="&ndash;";
			}
			
			if( addListeners )
			{
				var tag = links[3].textContent.replace( /\s+/g, '_' );
				
				//Plus
				links[1].setAttribute("onclick","return false;");
				links[1].addEventListener( "click", searchBoxChange(" "+tag+" ", " -"+tag+" "), false );
				
				//Minus
				links[2].setAttribute("onclick","return false;");
				links[2].addEventListener( "click", searchBoxChange(" -"+tag+" ", " "+tag+" "), false );
			}
		}
	}, 0 );
	
	function searchBoxChange(add, remove) { return function()
	{
		var searchBox = document.getElementById("tags");
		var value = " "+searchBox.value+" ";
		if( value.indexOf(remove) >= 0 )
			value = value.replace(remove," ");
		else if( value.indexOf(add) < 0 )
			value = add + value;
		
		//Trim excess whitespace from search box and give it focus
		searchBox.value = value.replace(/(^ +| +$)/g,'').replace(/  +/,' ');
		searchBox.focus();
	}}
}


/**
	Adds a new section to the taglist, complete with +/- links.
 */
function customTaglist(title, newTags)
{
	var tagList = !recursion && document.evaluate( "//section/ul/li/a[@class='wiki-link' and text() = '?']/../../..", document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null ).singleNodeValue;
	if( !tagList )
		return;
	if( !newTags || !(newTags instanceof Array ) || !newTags.length )
		throw "Parameter must be a non-empty array."
	
	var fragment = createElementX([{ tag:"h1", text:title }]);
	var newList = fragment.appendChild( createElementX({tag:"ul"}) );
	var tagsValue = document.getElementById("tags").value;
	if( tagsValue.length > 0 )
		tagsValue = '+'+tagsValue;
	
	for( var i = 0; i < newTags.length; i++ )
	{
		var newListItem = createElementX({ tag: "li", class:"category-0"/* general tag by default */ });
		var thisTag = newTags[i];//Make copy to avoid removing tag type from original settings
		
		if( thisTag.indexOf("art:") == 0 )
			newListItem.className = "category-1";
		else if( thisTag.indexOf("char:") == 0 )
			newListItem.className = "category-4";
		else if( thisTag.indexOf("copy:") == 0 )
			newListItem.className = "category-3";
		
		thisTag = thisTag.replace(/^(art|char|copy|gen):/,'');
		
		if( thisTag.indexOf(":") > 0 )
			newListItem.innerHTML = '<a class="wiki-link" href="/wiki_pages?title=help:cheatsheet">?</a>';
		else
			newListItem.innerHTML = '<a class="wiki-link" href="/wiki_pages?title='+thisTag.replace(/ /g,'_')+'">?</a>';
		
		newListItem.innerHTML += ' <a href="/posts?tags='+thisTag.replace(/ /g,'_')+tagsValue+'" class="search-inc-tag">+</a> <a href="/posts?tags=-'+thisTag.replace(/ /g,'_')+tagsValue+'" class="search-exl-tag">&ndash;</a>'+' <a href="/posts?tags='+thisTag.replace(/ /g,'_')+'" class="search-tag">'+thisTag.replace(/_/g,' ')+'</a>';
		newList.appendChild(newListItem);
	}
	
	tagList.appendChild(fragment);
}


/**
	Adds links to the navbar.
 */
function navbarLinks(links)
{
	if( !navbar || recursion || !links || !links.length )
		return;
	
	var fragment = createElementX([]);
	
	for( var i = 0; i < links.length; i++ )
		if( links[i].text && links[i].href )
			fragment.appendChild( createElementX({ tag: "li" }) )
				.appendChild( createElementX({ tag:"a", href:links[i].href, text:links[i].text }) );
	navbar.insertBefore( fragment, navbar.firstChild );
}


/**
	Removes the specified statistics from user profiles.
 */
function hideUserStatistics(deleteMe, deleteThem)
{
	var deleteArray, statBlock = !recursion && document.getElementsByClassName("user-statistics")[0];
	if( !statBlock )
		return;
	
	if( statBlock.parentNode.parentNode.getElementsByTagName("h1")[0].textContent == login )
		deleteArray = ( deleteMe   instanceof Array ) ? deleteMe   : [];//Your profile
	else
		deleteArray = ( deleteThem instanceof Array ) ? deleteThem : [];//Someone else's profile
	
	if( deleteArray.indexOf("Statistics") >= 0 )
		statBlock.parentNode.getElementsByTagName("h2")[0].style.display = "none";
	
	var statLabels = statBlock.getElementsByTagName("th");
	
	for( var i = 0; i < statLabels.length; i++ )
		if( deleteArray.indexOf( statLabels[i].textContent ) >= 0 )
			statLabels[i].parentNode.style.display = "none";
}


/**
	Applies style attributes to links to user pages depending on the type of user.

	refreshDays: Number of days to cache a specific user's level
	styleObj:    Collection of styles applied to the different user levels
	
	User level info @ danbooru/app/models/user.rb
 */
function styleUserLevels( refreshDays, styleObj )
{
	if( recursion )
		return;
	
	var userCache, loadAgain = true;
	
	MS_observeInserts( subStyle )();	
	
	function subStyle(e)
	{
		if( e && !e.target.getElementsByTagName )
			return;

		var userLinks = new XPathEvaluator().evaluate('descendant-or-self::a[contains(@href,"/users/")]', (e ? e.target : document), null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
		if( userLinks.snapshotLength > 0 ) setTimeout( function()
		{
			var missing = {};
			if( loadAgain )
			{
				loadAgain = false;
				setTimeout( function(){ loadAgain = true; }, 2000 );
				
				userCache = MS_getValue( "styleUserLevels.cache" ) || { saveName: "styleUserLevels.cache", expires: refreshDays*24*3600 };
			}
			
			for( var i = 0; i < userLinks.snapshotLength; i++ )
			{
				var link = userLinks.snapshotItem(i);
				if( /^https?:..[^\.]+.donmai.us.users.\d+/.test( link.href ) )
				{
					var index = link.href.replace( /.*users\//, 'id=' );
					var info = 0;
					
					if( link.textContent.indexOf("user #") < 0 && link.className )
					{
						if( link.className.indexOf("user-banned") >= 0 )
							info = { level:10 };
						else if( link.className.indexOf("user-member") >= 0 )
							info = { level:20 };
						else if( link.className.indexOf("user-gold") >= 0 )
							info = { level:30 };
						else if( link.className.indexOf("user-platinum") >= 0 )
							info = { level:31 };
						else if( link.className.indexOf("user-builder") >= 0 )
							info = { level:32 };
						else if( link.className.indexOf("user-contributor") >= 0 )
							info = { level:33 };
						else if( link.className.indexOf("user-janitor") >= 0 )
							info = { level:35 };
						else if( link.className.indexOf("user-moderator") >= 0 )
							info = { level:40 };
						else if( link.className.indexOf("user-admin") >= 0 )
							info = { level:50 };
					}
					
					if( !info )
						info = MS_getValue( index, userCache );
					
					if( info )
						setUserStyle( link, info );
					else if( !missing[index] )
						missing[index] = [ link ];
					else
						missing[index].push( link );
				}
			}
			for( elem in missing )
				getLevel( elem, missing[elem] );
		}, 1 );
	}
	
	function getLevel( index, linkArray )
	{
		MS_requestAPI( '/user/index.json?'+index, function(responseDetails)
		{
			var result;
			
			try{ result = MS_parseJSON(responseDetails.responseText); }
			catch(e) { getLevel( index, linkArray ); return; }
			
			for( var i = 0; i < result.length; i++ )
				MS_setValue( 'id='+result[i].id, { name:result[i].name, level:result[i].level }, refreshDays*24*3600, userCache );
							
			var info = MS_getValue( index, userCache ) || { level:20 };
			if( !info.name )
				MS_setValue( index, info, refreshDays*24*3600, userCache );//default level is Member
			
			for( var i = 0; i < linkArray.length; i++ )
				setUserStyle( linkArray[i], info );
		});
	}

	function setUserStyle( link, info )
	{
		var style, className = "";

		switch( info.level )
		{
			case 50: style = styleObj.Admin;       className = "user-admin";       break;
			case 40: style = styleObj.Mod;         className = "user-moderator";   break;
			case 35: style = styleObj.Janitor;     className = "user-janitor";     break;
			//case 34: style = styleObj.TestJanitor; break;//"TestJanitor": "color:#4CC417",
			case 33: style = styleObj.Contributor; className = "user-contributor"; break;
			case 32: style = styleObj.Builder;     className = "user-builder";     break;
			case 31: style = styleObj.Platinum;    className = "user-platinum";    break;
			case 30: style = styleObj.Gold;        className = "user-gold";        break;
			case 20: style = styleObj.Member;      className = "user-member";      break;
			case 10: style = styleObj.Blocked;     className = "user-banned";      break;
			default: style = styleObj.Unknown; break;//unknown power level
		}
		
		if( style && style.length > 0 )
		{
			if( /^color:[^;]+$/.test(style) )
				link.style.color = style.replace('color:','');
			else
				link.setAttribute( "style", link.getAttribute("style", "")+"; "+style );
		}
		if( className && link.className.indexOf(className) < 0 )
			link.className += (link.className ? ' ' : '')+className;
		if( info.name && /^user #\d+$/.test(link.textContent) )
			link.textContent = info.name;
	}
}


/**
	Adds back navigation by arrow keys.
 */
function arrowKeyNavigation(useArrows)
{
	if( recursion )
		return;
	
	var leftLink = 0, rightLink = 0;
	
	var links = document.getElementById("popular-nav-links") ? document.getElementById("popular-nav-links").getElementsByTagName("a") : [];
	if( links.length == 9 )
	{
		//Popular
		if( location.search.indexOf("scale=month") >= 0 )
		{
			leftLink = links[6];
			rightLink = links[8];
		}
		else if( location.search.indexOf("scale=week") >= 0 )
		{
			leftLink = links[3];
			rightLink = links[5];
		}
		else
		{
			leftLink = links[0];
			rightLink = links[2];
		}
	}
	else if( (links = document.getElementsByClassName("paginator")[0]) != null && (links = links.getElementsByTagName("li")).length > 0 )
	{
		//Paginator.  Link will be a <span> element if there are no more pages in the requested direction.
		leftLink = links[0].firstChild;
		rightLink = links[links.length - 1].firstChild;
	}
	else if( (links = document.getElementById("nav-links")) != null && (links = links.getElementsByTagName("li")[0]) != null )
	{
		leftLink = links.getElementsByClassName("prev")[0];
		rightLink = links.getElementsByClassName("next")[0];
	}
	else return;
	
	//Listen for left/right arrow key presses
	window.addEventListener( "keydown", function(key)
	{
		//Don't navigate if Alt/Ctrl/Shift are also being pressed, or if a text field has focus.
		if( key.altKey || key.ctrlKey || key.shiftKey || key.metaKey || /^(INPUT|TEXTAREA)$/.test(document.activeElement.tagName) )
			return;
		if( key.keyCode == 37 && leftLink && leftLink.href )
			leftLink.click();
		else if( key.keyCode == 39 && rightLink && rightLink.href )
			rightLink.click();
	}, false );
}


/**
	Appends images' scores and/or favorite counts below their thumbnails.
	
	appendScore: If true, append the score.
	appendFavcount: If nonzero, append the favcount.  This specifies the minutes to keep favcount in cache.
 */
function scoreThumbnails( appendScore, appendFavcount )
{
	if( recursion || !content )
		return;
	
	if( appendFavcount > 0 )
		appendFavcount *= 60;
	else
		appendFavcount = 0;
	
	setTimeout( function() { MS_observeInserts( giveSomething )(); }, 0 );
	
	var favcountCache;
	
	function giveSomething(e)
	{
		if( e && !e.target.getElementsByTagName )
			return;
		
		var thumbList = new XPathEvaluator().evaluate("descendant-or-self::node()[contains(@class,'post-preview')]", (e ? e.target : document), null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
		var favIDs = "", divList = [];
		
		if( !thumbList.snapshotLength )
			return;
		
		if( appendFavcount )
		{
			favcountCache = MS_getValue( "giveThumbnailsScores.favCount" ) || { saveName: "giveThumbnailsScores.favCount", expires: appendFavcount*2 };
		}
		
		for( var i = 0; i < thumbList.snapshotLength; i++ )
		{
			if( thumbList.snapshotItem(i).getElementsByClassName("misc_thumb_scores")[0]  )
				continue;
			
			/* Comment index has a different structure than thumbs elsewhere. */
			var imageBox = thumbList.snapshotItem(i).getElementsByTagName("img")[0].parentNode;
			var postID = thumbList.snapshotItem(i).getAttribute("data-id") || imageBox.href.replace(/.*\//,'');
			imageBox = imageBox.parentNode;
			
			/* The default height for thumbnails will cause most scores to be hidden, so
			   try to override the height here in case the user hasn't already done so. */
			imageBox.style.minHeight = "170px";
			
			var newDiv = imageBox.appendChild( createElementX({ tag: "div", class: "misc_thumb_scores", style:"text-align:center" }) );
			
			if( appendScore )
			{
				var score = thumbList.snapshotItem(i).getAttribute("data-score",0);
				newDiv.appendChild( createElementX("Score: "+score) );
				if( score < 0 )
					newDiv.style.color = "red";
			}
			if( appendFavcount )
			{
				var favcount = thumbList.snapshotItem(i).getAttribute("data-favcount");//Attribute added by this script.
				if( favcount != null )
					MS_setValue( thumbList.snapshotItem(i).id, favcount, appendFavcount, favcountCache );
				else
					favcount = MS_getValue( thumbList.snapshotItem(i).id, favcountCache );
				
				if( favcount != null )
					newDiv.appendChild( createElementX( " ("+favcount+" fav"+( favcount == 1 ? ")" : "s)" ) ) );
				else
				{
					divList.push({ "id":postID, "div":newDiv });
					favIDs += ( favIDs.length > 0 ? "," : "" ) + postID;
				}
			}
		}/* loop over thumbs */
		
		MS_requestAPI( "/posts.json?tags=status:any+id:"+favIDs, function(responseDetails)
		{
			var result;
			try{ result = MS_parseJSON(responseDetails.responseText); }
			catch(e){ return; }
			
			for( var i = 0; i < result.length; i++ )
			{
				MS_setValue( "post_"+result[i].id, result[i].fav_count, appendFavcount, favcountCache );
				for( var j = 0; j < divList.length; j++ )
					if( divList[j].id == result[i].id )
						divList[j].div.appendChild( createElementX( " ("+result[i].fav_count+" fav"+( result[i].fav_count == 1 ? ")" : "s)" ) ) );
			}
		});
	}
}


/**
	Blacklist implementation that replaces images (full and thumbnail) with links labeled with tags
	they matched in the blacklist.  Clicking the link/image alternates between the two.  Translation notes
	appear every other time the image is displayed.

	User and rating metatags are supported.  Your own uploads are exempt from the blacklist.
	
	blacklist: regex string containing the blacklisted tags
 */
function blacklistTags( blacklistStr )
{
	if( recursion )
		return;
	
	var blacklist;
	try{ blacklist = new RegExp( "(^|.* )("+blacklistStr+")( .*|$)", "i" ); }
	catch(e){ throw "Unable to parse blacklist regex"; }	
	
	var imageContainer = document.getElementById("image-container");
	var noteContainer = imageContainer && document.getElementById("note-container");
	
	setTimeout( function()
	{
		if( imageContainer )
		{
			var user = document.evaluate("//section/ul/li[contains(text(),'Uploader')]/a", document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null ).singleNodeValue.textContent;
			
			var rating = document.evaluate("//section/ul/li[contains(text(),'Rating:')]", document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null ).singleNodeValue.textContent;
			rating = rating.indexOf("Safe") > 0 ? 's' : ( rating.indexOf("Explicit") > 0 ? 'e' : 'q' );

			applyBlacklist( imageContainer, document.getElementsByName("tags")[0].content, user, rating );
		}
		
		MS_observeInserts( function(e)
		{
			var found = [];
			
			if( !e )
				found = document.getElementsByClassName("post-preview");
			else if( e.target.className && e.target.className.indexOf("post-preview") >= 0 )
				found = [ e.target ];
			else if( e.target.getElementsByClassName )
				found = e.target.getElementsByClassName("post-preview");
			
			for( var i = 0; i < found.length; i++ )
				applyBlacklist( found[i], found[i].getAttribute("data-tags"), found[i].getAttribute("data-uploader"), found[i].getAttribute("data-rating") );
		})();
	}, 10 );
	
	function applyBlacklist(elem, tags, user, rating)
	{
		if( !elem || !tags || user == login )
			return;
		
		tags += " user:"+user+" rating:"+rating;
		
		var badTags = [];//List of all of this post's tags that hit the blacklist
		while( blacklist.test(tags) )
		{
			badTags.push( tags.replace( blacklist, '$2' ) );
			tags = tags.replace( blacklist, '$1  $3' );
		}
		badTags.sort();
		
		var image = badTags.length && elem.getElementsByTagName("img")[0];
		
		if( !image )
			return;
		
		var blackLink = createElementX({ tag: "a", href:image.src, text:('hidden ('+badTags+')').replace(/,/g,', '), title:( image.title || "" ) });
		
		if( elem.className )
		{
			if( elem.className.indexOf("deleted") > 0 )
				blackLink.style.color = "#000";
			else if( elem.className.indexOf("pending") > 0 )
				blackLink.style.color = "#00F";
			else if( elem.className.indexOf("flagged") > 0 )
				blackLink.style.color = "#F00";
			else if( elem.className.indexOf("parent") > 0 )
				blackLink.style.color = "#CC0";
			else if( elem.className.indexOf("child") > 0 )
				blackLink.style.color = "#0F0";
		}
		
		if( image.parentNode.href )
		{
			//Thumbnails are surrounded by links.  Hide those instead of the images themselves.
			image = image.parentNode;
			blackLink.href = image.href;
			
			//Comments have div.post-preview/div.preview/a/img
			elem = image.parentNode;
			elem.style.textAlign = "center";
		}
		
		var showNotes = true;
		if( elem == imageContainer )
			noteContainer.style.display = "none";
		image.style.display = "none";
		
		elem.insertBefore( blackLink, elem.firstChild );
		
		blackLink.addEventListener( "click", function(e) { if( !e.ctrlKey ) e.preventDefault(); }, false );
		elem.addEventListener( "click", function(e)
		{
			if( e.ctrlKey ) return;
			e.preventDefault();

			//Swap styles
			var oldImageStyle = image.style.display || "none";
			image.style.display = blackLink.style.display || "block";
			blackLink.style.display = oldImageStyle;
			
			if( elem == imageContainer )
				noteContainer.style.display = ( image.style.display == "none" || (showNotes = !showNotes) ? "none" : "block" );
		}, true );
	}
}


/**
	Sets all times to the "X units ago" form with custom precision.
 */
function precisionTime(precision)
{
	if( recursion )
		return;
	
	MS_observeInserts( function(e)
	{
		if( e && !e.target.getElementsByTagName )
			return;
		
		var timeNodes = ( e ? e.target : document ).getElementsByTagName("time");
		for( var i = 0; i < timeNodes.length; i++ )
		{
			//<time datetime="2013-02-26T22:16-05:00" title="2013-02-26 22:16:25 -0500">2013-02-26 22:16</time>
			var unit = "", title = timeNodes[i].getAttribute("title").split(" ");
			var duration = ( Date.now() - new Date(title[0]+'T'+title[1]+title[2]) ) / 1000;
			//new Date(timeNodes[i].getAttribute("datetime")).getTime() - 1000*parseInt( (timeNodes[i].getAttribute("title") || "0 0:0:0").split(/[\s:]+/,4)[3], 10 );
			
			if( duration < 60 )
				unit = "second";
			else if( (duration /= 60) < 60 )
				unit = "minute";
			else if( (duration /= 60) < 24 )
				unit = "hour";
			else if( (duration /= 24) < 30.4375 )
				unit = "day";
			else if( (duration /= 30.4375) < 12 )
				unit = "month";
			else if( (duration /= 12) > 0 )
				unit = "year";
			else continue;//Parsing error
			
			if( precision > 0 && unit != "second" )
				timeNodes[i].textContent = duration.toFixed(precision)+" "+unit+"s ago";
			else
			{
				duration = ( duration <= 0 ? "0" : duration.toFixed(0) );
				timeNodes[i].textContent = duration+" "+unit+( duration == "1" ? " ago" : "s ago" );
			}
		}
	})();
}


/**
	Adds a pseudo-tag subscription to your profile.
	
	title:           Title of tag subscription; must be unique
	useTimestamp:    If true, append a timestamp to the title with each refresh
	refreshInterval: Hours to wait before refreshing the subscription
	maxThumbs:       Number of thumbnails to display
	isIntersect:     If true, find posts that contained in all the queries.  Otherwise, find posts in any query.
	queryList		 [ { search:"", pages#, filter:[""] }, ... ]
					 
					 The optional filter parameter finds the intersection of just that query and the tags in the filter.
					 Metatags and negated tags are not supported in the filter.
	
	If isIntersect is true, posts will be in the order of the final query.
	Otherwise, posts are sorted by descending ID.
	
	The subscription title points to a tag search using the tags passed to the first query.
    
	When the subscription refreshes, all queries will be rerun with every page load until all
	have completed on the same page.
 */
function addTagSubscription( title, useTimestamp, refreshInterval, maxThumbs, isIntersect, queryList )
{
	//Validate arguments
	if( recursion || !login )
		return;
	if( arguments.length == 1 && title instanceof Array )
	{
		for( var i = 0; i < title.length; i++ )
			addTagSubscription( title[i].title, title[i].timestamp, title[i].refresh, title[i].thumbs, title[i].intersect, title[i].queries );
		return;
	}
	if( !title )
		throw "No title";
	if( refreshInterval <= 0 )
		throw ""+title+": Invalid refresh interval";
	if( !(queryList instanceof Array) || !queryList.length )
		throw ""+title+": Invalid query list";
	if( maxThumbs <= 0 || maxThumbs > 100 )
		maxThumbs = 10;
	
	//Validate queries
	for( var i = 0; i < queryList.length; i++ )
	{
		if( typeof(queryList[i].search) != "string" )
			throw ""+title+": 'search' parameter missing or not a string";
		if( !queryList[i].pages )
			queryList[i].pages = 1;//Missing number of pages, assume 1.
		if( queryList[i].pages > 1000 )
			queryList[i].pages = 1000;//Limit 1000 pages (100k posts)
		if( !queryList[i].filter )
			queryList[i].filter = [];
	}
	
	var varPrefix = "addTagSubscription."+title;
	var subDiv = false;
	
	//Add the DIV to contain the thumbs to your own user page and append the thumbs if they already exist
	var userContent = document.getElementsByClassName("user-statistics").length && document.getElementById("a-show");
	if( userContent && ( location.pathname == "/users/"+loginID || userContent.getElementsByTagName("h1")[0].textContent == login ) )
	{
		subDiv = createElementX( { tag: "div", class:"box", id: varPrefix+"_thumbList" } );
		
		var thumbList = MS_getValue( varPrefix+"_thumbList" );
		if( thumbList )
			subDiv.innerHTML = thumbList;
		else
		{
			subDiv.appendChild( createElementX({ tag:"h2", title:JSON.stringify(queryList) }) ).appendChild( createElementX({ tag:"a", href:"/posts?tags="+queryList[0].search.replace(/ +/,'+'), text:title }) );
			if( MS_getValue( varPrefix+"_lock" ) )
				MS_setValue( varPrefix+"_lock", true, 30 );
		}
		
		userContent.appendChild(subDiv);
	}
	
	if( MS_getValue( varPrefix+"_lock" ) )
		return;
	
	var postResults = [], currentResults = [];
	runQuery(0, 1);
	
	function runQuery( argIndex, page )
	{
		//Set 10 second lock every time a query runs, to prevent concurrent queries
		MS_setValue( varPrefix+"_lock", true, 10 );
		
		if( argIndex >= queryList.length )
			return subBuildThumbs();//No more arguments, start building
		
		if( page == 1 )
			currentResults = [];//First page, no results yet
		if( page > queryList[argIndex].pages )
		{
			//No more pages.

			if( argIndex == 0 || !isIntersect )
				postResults = postResults.concat( currentResults );
			else
			{
				//Find the intersection between the previous queries and this one, using the order of this one.
				var oldPostResults = postResults;
				postResults = [];
				
				for( var i = 0; i < currentResults.length; i++ )
				{
					for( var j = 0; j < oldPostResults.length; j++ )
						if( oldPostResults[j]["id"] == currentResults[i]["id"] )
						{
							postResults.push( oldPostResults[j] );
							break;
						}
				}
				if( postResults.length == 0 )
					argIndex = queryList.length;//Intersection with empty set :(
			}
			return runQuery( argIndex + 1, 1 );
		}

		//Fetch next page
		MS_requestAPI( "/posts.json?tags="+queryList[argIndex].search.replace(/ +/g,'+')+"&page="+page, function(responseDetails)
		{
			var result;
			try { result = MS_parseJSON(responseDetails.responseText); }
			catch(e) { return runQuery( argIndex, page ); }
			
			if( queryList[argIndex].filter.length == 0 )
				currentResults = currentResults.concat( result );
			else for( var i = 0; i < result.length; i++ )
			{
				//Only add posts that contain all of the tags in the filter
				var pTags = result[i]["tag_string"].toLowerCase().split(' '), lastIdx = 0;
				for( var j = 0; lastIdx >= 0 && j < queryList[argIndex].filter.length; j++ )
				{
					if( queryList[argIndex].filter[j].indexOf("user:") != 0 )
						lastIdx = pTags.indexOf( queryList[argIndex].filter[j] );
					else if( queryList[argIndex].filter[j].replace(/_/g,' ').toLowerCase() != "user:"+result[i]["uploader_name"].toLowerCase() )
						lastIdx = -1;
				}
				if( lastIdx >= 0 )
					currentResults.push( result[i] );
			}
			
			//Go to next page if less than the maximum amount was returned, or if we have enough thumbs already
			if( result.length < 100 || ( queryList.length == 1 && currentResults.length > maxThumbs ) )
				page = 9999;
			
			//Get next page
			runQuery( argIndex, page + 1 );
		});
	}
	
	function subBuildThumbs()
	{
		//Sort union subscriptions by decreasing post ID
		if( !isIntersect && queryList.length > 1 )
			postResults.sort( function(a,b) { return( b["id"] - a["id"] ); } );
		
		//Remove duplicates up to the point where it doesn't matter
		for( var i = 1; i < postResults.length && i < maxThumbs; i++ )
		{
			for( var j = i - 1; j > 0; j-- )
				if( postResults[j]['id'] == postResults[i]['id'] )
				{
					//An earlier post already has this ID, so remove this post
					postResults.splice( i--, 1 );
					break;
				}
		}

		if( subDiv )
			subDiv.innerHTML = "";//Remove the old title that may have been added at the start
		else
			subDiv = createElementX({ tag:"div", class:"box" });//Dummy DIV
		
		function zeroIt(a) { return( a < 10 ? "0"+a : a ); }
		var tdate = new Date();
		var timestamp = " ("+(tdate.getMonth()+1)+"-"+zeroIt(tdate.getDate())+" "+zeroIt(tdate.getHours())+":"+zeroIt(tdate.getMinutes())+")";
		
		subDiv.appendChild( createElementX({ tag:"h2", title:JSON.stringify(queryList)+timestamp }) ).appendChild( createElementX([ { tag:"a", href:"/posts?tags="+queryList[0].search.replace(/ +/,'+'), text:title }, ( useTimestamp ? timestamp : "" ) ]) );
		subDiv.appendChild( createElementX({ tag:"div", class:"box" }) ).appendChild( buildThumbs( postResults, maxThumbs ) );
		
		MS_setValue( varPrefix+"_thumbList", subDiv.innerHTML, 7*24*3600 );
		MS_setValue( varPrefix+"_lock", true, refreshInterval * 3600 );
	}
}


/**
	Runs a truncated "Find artist" search on posts with URL sources but no artist tag and displays the results
	below the source, along with the number of other posts returned by a source search.
 */
function sourceStatLinks( findArtist, findSource )
{
	var source = !recursion && document.getElementById("post_source");
	var sourceStat = source && source.value.indexOf("http") == 0 && document.evaluate("//aside[@id='sidebar']/section/ul/li[contains(text(),'Source:')]", document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
	if( !sourceStat )
		return; 
	
	source = source.value;

	var postSearch = "tags=status:any+source:"+source.replace( /[^\/]+$/, '' )+"*";
	var pixivArtist = 0, statLink = sourceStat.childNodes[1];
	if( statLink.href && /\.pixiv.net\/(img|img[0-9]+\/img)\/[^ \/]+\/[0-9]+/.test(source) )
	{
		pixivArtist = source.replace(/(.*\/img\/|\/.*)/g,'');
		postSearch = "tags=status:any+source:pixiv/"+pixivArtist+"/*";
		//statLink.href += '#'+pixivArtist;
	}

	var findArtistDiv, sourceSearchDiv;
	if( (findArtist || findSource) && !document.getElementById("tag-list").getElementsByClassName("category-1")[0] )
	{
		findArtistDiv   = createElementX({tag:"div", style:"margin-left:1.5em; word-wrap:break-word; display:none" });
		sourceSearchDiv = createElementX({tag:"div", style:"margin-left:1.5em; word-wrap:break-word; display:none" });
		sourceStat.parentNode.insertBefore( createElementX([ findArtistDiv, sourceSearchDiv ]), sourceStat.nextSibling );
		
		if( findArtist )
			artistSearch();
		if( findSource )
			sourceSearch();
	}
	
	function artistSearch()
	{
		MS_requestAPI( '/artists.json?search[name]='+source, function(responseDetails)
		{
			var result;
			try{ result = MS_parseJSON(responseDetails.responseText); }
			catch(e){ return artistSearch(); }
			
			if( result.length < 4 )
			{
				for( var i = 0; i < result.length; i++ )
				{
					findArtistDiv.innerHTML += '<li class="category-1"><a href="/artists/show_or_new?name='+result[i]["name"]+'" onclick="document.getElementById(\'post-edit-link\').click(); Danbooru.RelatedTag.toggle_tag({target:this,preventDefault:function(){}}); return false;">'+result[i]["name"]+'</a></li>';
				}
				findArtistDiv.style.display = "block";
			}
		});
	}
	
	//Source search, clipping off the filename
	function sourceSearch()
	{
		MS_requestAPI( "/posts.json?"+postSearch, function(responseDetails)
		{
			var result;
			try{ result = MS_parseJSON(responseDetails.responseText); }
			catch(e){ return sourceSearch(); }

			if( result.length == 100 )
				result = "At least 99 other posts";
			else if( result.length == 2 )
				result = "1 other post";
			else
				result = ( result.length ? result.length - 1 : 0 )+" other posts";
			
			sourceSearchDiv.innerHTML = '<a href="/posts?'+postSearch+'">'+result+'</a>';
			sourceSearchDiv.style.display = "block";
		});
	}
}


/**
	Applies a color the first time a downvote link is clicked, only letting the vote through
	if it (and nothing else) is clicked again.
 */
function doubleDown(color)
{
	if( recursion || !color || !content )
		return;

	MS_observeInserts( function(e)
	{
		var down = document.evaluate(".//li/a[@data-method and contains(@href,'votes?score=down')]", (e ? e.target : document), null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
		for( var i = 0; i < down.snapshotLength; i++ )
		{
			var sLink = down.snapshotItem(i);
			var fLink = createElementX({ tag:"a", href:"#", text:sLink.textContent, onclick:"return false;", title:"Click twice to vote down." });
			
			sLink.parentNode.insertBefore( fLink, sLink );
			sLink.style.display = 'none';
			sLink.style.color = color;
			
			content.addEventListener("click", (function(firLink,secLink){ return function(e)
			{
				if( e && e.target == firLink )
				{
					firLink.style.display = "none";
					secLink.style.display = "inline";
				}
				else
				{
					firLink.style.display = "inline";
					secLink.style.display = "none";
				}
			}; })( fLink, sLink ), false );
		}
	} )();
}


//=====================================================================================
//=============================== HELPER FUNCTIONS ====================================
//=====================================================================================


function nbsp(count)
{
	var result = "";
	while( count-- > 0 )
		result += "\u00a0";
	return result;
}

function createElementX(obj)
{
	if( arguments.length > 1 || (arguments = obj) instanceof Array )
	{
		var fragment = document.createDocumentFragment();
		for( var i = 0; i < arguments.length; i++ )
			fragment.appendChild( createElementX( arguments[i] ) );
		return fragment;
	}
	
	if( obj instanceof Element )
		return obj;
	else if( typeof(obj) == "string" )
		return document.createTextNode(obj);
	else if( !obj.tag )
	{
		if( obj.childNodes )
			return obj;
		if( elem in obj )
			return document.createTextNode(""+obj);
		return document.createDocumentFragment();// {}
	}
	
	var elem = document.createElement(obj.tag);
	for( var key in obj )
	{
		if( key == "text" )
			elem.textContent = obj[key];
		else if( key != "tag" )
			elem.setAttribute(key, obj[key]);
	}
	return elem;
}

function MS_setValue( key, value, expires, memObj )
{
	//If 0 or less, value never expires
	if( expires > 0 )
		expires = Math.floor( new Date().getTime()/1000 + expires );
	else
		expires = 0;
	
	var i, keyList = [];
	
	if( !memObj )
	{
		GM_setValue( key, JSON.stringify({ "value": value, "expires": expires }) );
		try{ keyList = MS_parseJSON( GM_getValue( "Miscellaneous.keyList" ) ) || []; }
		catch(err){ keyList = []; }
	}
	else
	{
		keyList = memObj.keyList = MS_parseJSON( memObj.keyList ) || [];
		memObj[key] = { "value": value, "expires": expires };
		MS_setMemObj( memObj, "MS_setValue" );
	}
	
	if( !(keyList instanceof Array) )
		throw "MS_setValue(): Invalid keyList ("+typeof(keyList)+"): "+keyList;
	
	//Find insertion point, deleting duplicates along the way
	for( i = 0; i < keyList.length && keyList[i].expires <= expires; i++ )
		if( keyList[i].key == key )
			keyList.splice( i--, 1 );
	keyList.splice( i, 0, { "key":key, "expires":expires });
	
	if( !memObj )
		GM_setValue( "Miscellaneous.keyList", JSON.stringify(keyList) );
}

function MS_getValue( key, defaultValue, memObj )
{
	if( !memObj && typeof(defaultValue) == "object" && defaultValue.saveName )
	{
		memObj = defaultValue;
		defaultValue = null;
	}
	if( memObj )
	{
		if( !memObj[key] || ( memObj[key].expires > 0 && memObj[key].expires < new Date().getTime()/1000 ) )
			return defaultValue;
		return memObj[key].value;
	}
	
	var value = GM_getValue( key );
	
	if( !value )
		return defaultValue;
	
	try{ value = MS_parseJSON( value ); }
	catch(e) { return defaultValue; }
	
	if( value.expires > 0 && value.expires < new Date().getTime()/1000 )
		return defaultValue;
	
	return value["value"];
}

function MS_deleteValue( key, memObj )
{
	var keyList;
	if( memObj )
		keyList = memObj.keyList = MS_parseJSON( memObj.keyList ) || [];
	else
	{
		try{ keyList = MS_parseJSON( GM_getValue( "Miscellaneous.keyList" ) ) || []; }
		catch(err){ keyList = []; }
	}
	
	if( !(keyList instanceof Array) )
		throw "MS_deleteValue(): Invalid keyList ("+typeof(keyList)+"): "+keyList;
	
	var now = new Date().getTime()/1000;
	
	for( var i = keyList.length - 1; i >= 0; i-- )
		if( keyList[i].key == key )
		{
			keyList.splice(i,1);
			
			if( !memObj )
			{
				GM_deleteValue( key );
				GM_setValue( "Miscellaneous.keyList", JSON.stringify(keyList) );
			}
			else
			{
				delete memObj[key];
				MS_setMemObj( memObj, "MS_deleteValue" );
			}
		}
}

function MS_clearValues(num, memObj)
{
	if( num <= 0 )
		return;
	
	var keyList;
	if( memObj )
		keyList = memObj.keyList = MS_parseJSON( memObj.keyList ) || [];
	else
	{
		try{ keyList = MS_parseJSON( GM_getValue( "Miscellaneous.keyList" ) ) || []; }
		catch(err){ keyList = []; }
	}
	var now = new Date().getTime()/1000;
	var oldLen = keyList.length;
	
	for( var i = 0; num > 0 && i < keyList.length && keyList[i].expires < now; i++ )
		if( keyList[i].expires > 0 )
		{
			num--;
			GM_deleteValue( keyList.splice( i--, 1 )[0].key );
		}
	
	if( oldLen != keyList.length && !memObj )
		GM_setValue( "Miscellaneous.keyList", JSON.stringify(keyList) );
} MS_clearValues(1);

function MS_setMemObj(obj,func)
{
	if( obj._timer != null )
		clearTimeout( obj._timer );
	obj._timer = setTimeout( function()
	{
		delete obj._timer;
		MS_clearValues( 20, obj );
		MS_setValue( obj.saveName, obj, obj.expires );
	}, 1500 );
}

function MS_parseJSON(text)
{
	if( text instanceof Array )
	{
		//Parse all elements in arrays of strings
		for( var i = 0; i < text.length && typeof(text[i]) == "string"; i++ )
			try{ text[i] = MS_parseJSON( text[i] ); }
			catch(err){ throw "Bad JSON in text["+i+"]: "+text[i]; }
		return text;
	}
	else if( typeof(text) == "string" ) return JSON.parse(text, function(key,val)
	{
		//Recursively parse strings that look like objects
		if( typeof(val) == "string" )
		{
			try{ return MS_parseJSON(val); }
			catch(err){}
		}
		return val;
	});
	//Not a string or array, so return unchanged
	return text;
}

function MS_addStyle(text)
{
	if( text instanceof Array )
	{
		for( var i = 0; i < text.length; i++ )
			MS_addStyle( text[i].style ? text[i].style : text[i] );
	}
	else if( typeof(text) == "string" )
	{
		var style = createElementX({ tag:'style', type:'text/css' });
		style.innerHTML = text.replace(/;/g,'!important;')
							  .replace(/(!\s*important\s*)+\}/gi,'}')//Remove existing instances of "!important" to avoid duplicates
							  .replace(/\}/gi,'!important}');//Add "!important" to everything
		document.getElementsByTagName('head')[0].appendChild(style);
	}
	else throw "MS_addStyle(): Invalid type: "+text;
}
 
function MS_alert( messageP )
{
	//Creates a large "alert" box that collects multiple alerts and doesn't pause the script
	var showAlerts = true;
	var dialog = document.body.appendChild( createElementX({ tag:"div", style:"position:fixed; left:10%; top:10%; z-index:10;	background-color:white;	display:none; width:20em; height:20em; border:2px solid black; padding:20px 20px 20px 20px; overflow:auto" }) );
	
	var header = dialog.appendChild( createElementX({ tag:"h4", text: "Miscellaneous Tweaks " }) );
	dialog.appendChild( createElementX({ tag:"hr" }) );
	var text = dialog.appendChild( createElementX({ tag:"div" }) );
	
	header.appendChild( createElementX({ tag:"input", type:"button", value:"Clear", title:"Clear all warnings and hide this alert." }) ).addEventListener( "click", function(){ dialog.style.display = "none"; text.innerHTML = ""; } );
	header.appendChild( createElementX(" ") );
	header.appendChild( createElementX({ tag:"input", type:"button", value:"Hide", title:"Hide this alert without clearing its contents." }) ).addEventListener( "click", function(){ dialog.style.display = "none"; } );
	header.appendChild( createElementX(" ") );
	header.appendChild( createElementX({ tag:"input", type:"button", value:"Stop", title:"Stop additional alerts from being triggered." }) ).addEventListener( "click", function(){ showAlerts = false; } );
	
	MS_alert = function(message)
	{
		if( showAlerts )
		{
			text.appendChild( createElementX( ""+message, { tag:"hr" }) );
			dialog.style.display = "block";
			dialog.style.width = "80%";
			dialog.style.height = "60%";
		}
	}
	MS_alert(messageP);
}

function MS_observeInserts(funcP)
{
	if( !content || recursion )
		return funcP;
	
	//Listen to node insertions only under id=content node
	var funcList = [ ];
	function mutateManager(e)
	{
		for( var i = 0; i < funcList.length; i++ )
		{
			try{ funcList[i](e); }
			catch(err){ GM_log("Tweaks - Insertion error - "+funcList[i].toString().replace(/\(.*/,'()')+": "+err); }
		}
	}
	
	var MutObj = window.MutationObserver || window.WebKitMutationObserver;//Firefix and Chrome but not Opera
	if( MutObj )
	{
		var monitor = new MutObj( function(mutationSet){
			mutationSet.forEach( function(mutation){
				for( var i = 0; i < mutation.addedNodes.length; i++ )
					if( mutation.addedNodes[i].nodeType == 1 )
						mutateManager({ "target":mutation.addedNodes[i] });
			});
		});
		monitor.observe( content, { childList:true, subtree:true } );
	}
	else content.addEventListener( "DOMNodeInserted", mutateManager, false );
	
	MS_observeInserts = function(func)
	{
		if( !content || recursion )
			return function(){};
		funcList.push( func );
		return func;
	};
	return MS_observeInserts(funcP);
}

/** Makes request to API link, e.g. "/posts.json". If "limit=" isn't included, limit=100 is added to the end. */
function MS_requestAPI( link, loadFun, errFun )
{
	GM_xmlhttpRequest(
	{
		method: "GET",
		url:location.protocol+"//"+location.host+link+( link.indexOf("limit=") < 0 ? ( link.indexOf("?") > 0 ? "&limit=100" : "?limit=100" ) : "" ),
		onload:loadFun,
		onerror:( errFun ? errFun : function(err){ MS_requestAPI( link, loadFun ); } )
	});
}

/** Helper method to construct a document fragment of thumbnails from post API results */
function buildThumbs(json, limit)
{
	if( limit <= 0 )
		limit = 100;
	
	var i, result = document.createDocumentFragment();
	
	if( typeof(json) == "string" )
		try{ json = MS_parseJSON(json); } catch(e){ return result; }
	
	for( i = 0; limit-- > 0 && i < json.length; i++ )
	{
		var borderColors = [], imgStyle = "", status = "post-preview", flag = "";

		if( json[i].has_children )
		{
			borderColors.push("#0F0");
			status += " post-status-has-children";
		}
		if( json[i].parent_id )
		{
			borderColors.push("#CC0");
			status += " post-status-has-parent";
		}
		
		if( json[i].is_deleted )
		{
			borderColors.push("#000");
			status += " post-status-deleted";
			flag = "deleted";
		}
		else if( json[i].is_pending )
		{
			borderColors.push("#00F");
			status += " post-status-pending";
			flag = "pending";
		}
		else if( json[i].is_flagged )
		{
			borderColors.push("#F00");
			status += " post-status-flagged";
			flag = "flagged";
		}
		
		if( borderColors.length > 1 )
		{
			//	app/assets/javascripts/posts.js 
			imgStyle = "border-color: "
			if( borderColors.length == 3 )
				imgStyle += borderColors[0]+" "+borderColors[2]+" "+borderColors[2]+" "+borderColors[1]; 
			else
				imgStyle += borderColors[0]+" "+borderColors[1]+" "+borderColors[1]+" "+borderColors[0];
			//$img.css("border", "2px solid");
		}
		
		var article = createElementX({ "tag":"article",
				"class":status,
				"id":"post_"+json[i].id,
				"data-id":json[i].id,
				"data-tags":json[i].tag_string,
				"data-uploader":json[i].uploader_name,
				"data-rating":json[i].rating,
				"data-width":json[i].image_width,
				"data-height":json[i].image_height,
				"data-flags":flag,
				"data-parent-id":( json[i].parent_id || "" ),
				"data-has-children":json[i].has_children,
				"data-score":json[i].score,
				"data-favcount":json[i].fav_count });
		article.appendChild( createElementX({ tag:"a", href:"/posts/"+json[i].id }) ).appendChild( createElementX({ tag:"img", src:"/ssd/data/preview/"+json[i].md5+".jpg", alt:json[i].tag_string, title:json[i].tag_string+" user:"+json[i].uploader_name+" rating:"+json[i].rating+" score:"+json[i].score, style:imgStyle }) );
		result.appendChild(article);
	}
	return result;
}