Unlimited Paginator Works

Makes any(?) page with a paginator on various Danbooru clones "bottomless"--blend pages seamlessly or separate each with a paginator.

As of 2015-08-20. See the latest version.

// ==UserScript==
// @name           Unlimited Paginator Works
// @namespace      https://greasyfork.org/scripts/5250
// @description    Makes any(?) page with a paginator on various Danbooru clones "bottomless"--blend pages seamlessly or separate each with a paginator.
// @include        http://behoimi.org/*
// @include        http://www.behoimi.org/*
// @include        http://danbooru.donmai.us/*
// @include        http://hijiribe.donmai.us/*
// @include        http://sonohara.donmai.us/*
// @include        http://gelbooru.com/*
// @include        http://www.gelbooru.com/*
// @include        http://konachan.tld/*
// @include        https://yande.re/*
// @include        https://chan.sankakucomplex.com/*
// @version        2015.08.19
// ==/UserScript==

//If true, each added page retains its paginator.  If false, elements are smoothly joined together.
var pageBreak = false;

//Minimum amount of window left to scroll, maintained by loading more pages.
var scrollBuffer = 600;

//Time (in ms) the script will wait for a response from the next page before attempting to fetch the page again.  If the script gets trapped in a loop trying to load the next page, increase this value.
var timeToFailure = 15000;

//============================================================================
//=========================Script initialization==============================
//============================================================================

var nextPage, mainTable, mainParent, pending, timeout, iframe; 

if( typeof(customF) != "undefined" )
	customF();

initialize();
function initialize()
{
	//Stop if inside an iframe
	if( window != window.top || scrollBuffer == 0 )
		return;
		
	//Stop if no "table"
	mainTable = getMainTable(document);
	if( !mainTable )
		return;
		
	//Stop if no paginator
	var paginator = getPaginator(document);
	if( !paginator )
		return;

	//Stop if no more pages
	nextPage = getNextPage(paginator);
	if( !nextPage )
		return;

	//Hide the blacklist sidebar, since this script breaks the tag totals and post unhiding.
	var sidebar = document.getElementById("blacklisted-sidebar");
	if( sidebar )
		sidebar.style.display = "none";

	//Other important variables:
	scrollBuffer += window.innerHeight;
	mainParent = mainTable.parentNode;
	pending = false;
	
	iframe = document.createElement("iframe");
	iframe.addEventListener("load", appendNewContent, false);
	iframe.width = iframe.height = 0;
	iframe.style.visibility = "hidden";
	document.body.appendChild(iframe);

	//Stop if empty page
	if( /<p>(Nothing to display.|Nobody here but us chickens!)<.p>/.test(mainTable.innerHTML) )
		return;

	//Add copy of paginator to the top
	mainParent.insertBefore( paginator.cloneNode(true), mainParent.firstChild );

	if( !pageBreak )
		paginator.style.display = "none";//Hide bottom paginator
	else
	{
		//Reposition bottom paginator and add horizontal break
		mainTable.parentNode.insertBefore( document.createElement("hr"), mainTable.nextSibling );
		mainTable.parentNode.insertBefore( paginator, mainTable.nextSibling );
	}
		
	//Listen for scroll events
	window.addEventListener("scroll", testScrollPosition, false);
	testScrollPosition();
}

//============================================================================
//============================Script functions================================
//============================================================================

//Some pages match multiple "tables", so order is important.
function getMainTable(source)
{
	//Special case #1: Sankaku post index with Auto Paging enabled
	if( /sankaku/.test(location.host) && /auto_page=1/.test(document.cookie) && /^(post(\/|\/index\/?)?|\/)$/.test(location.pathname) )
		return null;
		
	var xpath =
	[
		 ".//div[@id='posts']/div"						// Danbooru 2; don't want to fall through to the wrong xpath if no posts ("<article>") on first page.
		,".//div[@id='c-pools']//section"				// Danbooru 2
		,".//div[@id='a-index']//table[not(contains(@class,'search'))]"	// Danbooru 2
		,".//div[@id='a-index']"						// Danbooru 2
		
		,".//table[contains(@class,'highlight')]"		// large number of pages
		,".//div[@id='content']/div/div/div/div/span[@class='author']/../../../.."	// Sankaku: note search
		,".//div[contains(@id,'comment-list')]/div/.."	// comment index
		,".//*[not(contains(@id,'popular'))]/span[contains(@class,'thumb')]/a/../.."	// post/index, pool/show, note/index
		,".//li/div/a[contains(@class,'thumb')]/../../.."	// post/index, note/index
		,".//div[@id='content']//table/tbody/tr[@class='even']/../.."	// user/index, wiki/history
		,".//div[@id='content']/div/table"				// 3dbooru user records
		,".//div[@id='forum']"							// forum/show
		,".//div[@id='content']"						// Gelbooru forum thread
		,".//div/span[contains(@class,'thumb')]/.."		// Gelbooru posts
	];
	
	for( var i = 0; i < xpath.length; i++ )
	{
		getMainTable = (function(query){ return function(source) { return new XPathEvaluator().evaluate(query, source, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue; }; })( xpath[i] );
		var result = getMainTable(source);
		if( result )
		{
			//alert("UPW main table query: "+xpath[i]+"\n"+location.pathname);
			return result;
		}
	}
	
	return null;
}

function getPaginator( source )
{
	var pager = new XPathEvaluator().evaluate("descendant-or-self::div[@id='paginator' or @class='paginator']", source, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
	
	// Need clear:none to prevent the 2nd page from being pushed to below the sidebar on the Post index... but we don't want this when viewing a specific pool,
	// because then the paginator is shoved to the right of the last images on a page.  Other sites have issues with clear:none as well, like //yande.re/post.
	if( pager && location.host.indexOf("donmai.") >= 0 && document.getElementById("sidebar") )
		pager.style.clear = "none";
	
	return pager;
}

function getNextPage( source )
{	
	var page = getPaginator(source);
	if( page )
		page = new XPathEvaluator().evaluate(".//a[@alt='next' or contains(text(),'>') or contains(text(),'Next')]", page, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue || /*Gelbooru:*/page.querySelector(".pagination b + a");
	return( page ? page.href : null );
}

function testScrollPosition()
{
	if( !nextPage )
		testScrollPosition = function(){};
	
	//Take the max of the two heights for browser compatibility
	else if( !pending && window.pageYOffset + scrollBuffer > Math.max( document.documentElement.scrollHeight, document.documentElement.offsetHeight ) )
	{	
		pending = true;
		timeout = setTimeout( function(){pending=false;testScrollPosition();}, timeToFailure );
		iframe.contentDocument.location.replace(nextPage);
	}
}

function appendNewContent()
{
	//Make sure page is correct
	clearTimeout(timeout);
	if( iframe.contentDocument.location.href != nextPage )
	{
		setTimeout( function(){ pending = false; }, 1000 );
		return;
	}

	//Copy content from retrived page to current page, but leave off certain headers, labels, etc...
	var sourcePaginator = document.adoptNode( getPaginator(iframe.contentDocument) );
	var nextElem, deleteMe, source = document.adoptNode( getMainTable(iframe.contentDocument) );
	
	if( /<p>(Nothing to display.|Nobody here but us chickens!)<.p>/.test(source.innerHTML) )
		nextPage = null;
	else
	{
		nextPage = getNextPage(sourcePaginator);

		if( pageBreak )
			mainParent.appendChild(source);
		else
		{
			//Hide elements separating one table from the next (h1 is used for user names on comment index)
			var rems = source.querySelectorAll("h2, h3, h4, thead, tfood");
			for( var i = 0; i < rems.length; i++ )
				rems[i].style.display = "none";
			
			//Move contents of next table into current one
			var fragment = document.createDocumentFragment();
			while( (nextElem = source.firstChild) )
				fragment.appendChild(nextElem);
			mainTable.appendChild(fragment);
		}
	}

	//Add the paginator at the bottom if needed.
	if( !nextPage || pageBreak )
		mainParent.appendChild( sourcePaginator );
	if( pageBreak && nextPage )
		mainParent.appendChild( document.createElement("hr") );
	
	//Clear the pending request marker and check position again
	pending = false;
	testScrollPosition();
}

// I am the code of my script.
// HTML is my body, and JavaScript is my blood.
// I have incorporated over a thousand paginators.
// Unaware of loss.
// Nor aware of gain.
// Withstood boredom to include many pages,
// Striving for the script's completion.
// I have no regrets, this is the only path.
// My whole life was "Unlimited Paginator Works."