(function ( $ ) {
// {{documentation}}
// This is a (very) modified version of User:Atelaes/TabbedLanguages.js.
// Tabbed languages with tabs on the side.
// Tabs design by [[User:Jorm (WMF)]]

/*jshint maxerr:1048576, strict:true, undef:true, latedef:true, es5:true */
/*global mw, jQuery, importScript, importScriptURI, $, ObjectStorage */

if (($.cookie('disable-tabbed-languages') !== null) || (location.search.indexOf("tabbedlanguages=off") !== -1))
	return;
if (!((mw.config.get('wgNamespaceNumber') === 0) || (mw.config.get('wgPageName') === "Wiktionary:Sandbox")))
	return;

var bodyContent = $(".mw-content-ltr .mw-parser-output")[0], // NOT #bodyContent
	languageLinks, ttr,
	languageButtons,
	caption,
	bodyContentFragment,
	complete = false,
	MO = window.MutationObserver || window.WebKitMutationObserver,
	observerStyleSheet;

// Setting up the tabs has not yet been started.
function makeTabsfromScratch() {
	// Set up the variables...
	window.tabbedLanguages = [];
	window.languageContainers = [];
	window.currentLanguageTab = 0;
	window.languageHeaderEditButtons = [];
	languageButtons = [];
	window.tabstable = newNode('table', {id: 'tabstable'},
		newNode('tbody',
			ttr = newNode('tr',
				newNode('td', {'style': 'padding-top:0px;vertical-align:top;'},
					newNode('table', {'style': 'margin-top: -2px;'},
						languageLinks = newNode('tbody', {id: 'languageLinks'}))))));
	window.loadremovecatbuttons = false;

	bodyContentFragment = document.createDocumentFragment();

	// If bodyContent is complete, do it all at once.
	// Otherwise, only start working, but don't attempt the whole thing.
	// Use catlinks+*, as gEBCN isn't always available.
	var catlinks = document.getElementById( "catlinks" );
	if( bodyContent && catlinks && catlinks.nextSibling ) {
		allAtOnce();
	} else {
		createObserver();
	}
}

function allAtOnce() {
	var languageContainer, toc = document.getElementById( "toc" );
	currentLanguageTab = 0;
	while ( bodyContent.firstChild ) {
		bodyContentFragment.appendChild( bodyContent.firstChild );
	}
	try {
		for( var child = bodyContentFragment.firstChild; child && !isHeader( child ); ){
			child = child.nextSibling;
		}
		for (
			child = child && bodyContentFragment.insertBefore(tabstable, child).nextSibling;
			child && child.className !== 'printfooter' && child.className !== 'catlinks';
			child = child.nextSibling
		) {
			if ( isHeader( child ) ) {
				var langspan = child.getElementsByClassName('mw-headline')[ 0 ],
					language = langspan && ( langspan.innerText || langspan.textContent );

				if ( language ) {
					newTab( tabbedLanguages.push(language) - 1, language );
					processEditButton( child.getElementsByClassName('mw-editsection')[ 0 ] );
					// should probably be set from a return value of above.
					languageContainer = languageContainers[ languageContainers.length - 1 ];

					bodyContentFragment.removeChild( child );

					child = tabstable;

				}
			} else {
				if ( child.nodeName !== "HR" ) {
					languageContainer.insertBefore(child, languageContainer.lastChild);
				} else {
					bodyContentFragment.removeChild(child);
				}
				child = tabstable;
			}
		}

		if( tabbedLanguages.length ) {
			if( toc ) {
				toc.parentNode.removeChild( toc );
			}
			sortCats();
			bodyContent.appendChild( bodyContentFragment );
			setUpHashChange()();
			if( location.hash === '' ) {
				location.replace( "#" + tabbedLanguages[ currentLanguageTab ] );
			}
			complete = true;
		} else {
			bodyContent.appendChild( bodyContentFragment );
		}
	} catch( e ) {
		window.console && console.error( e );
		bodyContent.appendChild( bodyContentFragment );
		complete || setUpHashChange()();
	}
}

// Set up a MutationObserver to detect when new elements are loaded.
function createObserver() {

	// If .ready happens early (or MO and animstart aren't supported), go ahead.
	if( $.isReady ) {
		return; // ??? How did we get $.isReady if bC or catlinks aren't loaded?
	} else {
		$( document ).ready( function () {
			if( tabbedLanguages.length === 0 ) {
				removeObserver();
				if( !bodyContent ) {
					bodyContent = $(".mw-content-ltr")[0];
				}
				if( bodyContent ) {
					allAtOnce();
				}
			}
		});
	}

	if( !document.getElementsByClassName ) {
		return;
	}

	if( !MO ) {
		var supportsAnimations = false;
		// Check if we can use animations as a fallback. If not, abort.
		// Basically yoinked from Modernizr.
		$.each(
			"animationName WebkitAnimationName MozAnimationName OAnimationName msAnimationName".split(" "),
			function(a, b){
				// Do documentElements even always have .style? If not, this'll need fixing.
				if( document.documentElement.style[ b ] !== undefined ) {
					supportsAnimations = true;
					return false;
				}
			}
		);
		if( supportsAnimations === false ) {
			return;
		}
	}

	var foundHeader = false,
		toc, recentChild,
		timer = false, // It's the setTimeout value, or false otherwise.
		recentHeader = -1,
		checkTab, tabFound = false,
		// If anyone on this project even considers making a random element
		// on a page have the class "visualClear"...
		visualClear = document.getElementsByClassName( "visualClear" ),
		languageContainer,
		observer;

	// TODO: Deal with the little jumping elements below the tabstable.
	// ...How?

	function elemFound() {
		if( timer === false ) {
			timer = setTimeout( function() {
				reactToObserver();
				timer = false;
			}, 1);
		}
	}

	if( MO ) {
		observer = new MO( elemFound );
		observer.observe( document, { childList: true, subtree: true } );
	} else {
		// Fallback for browsers that don't support MO, but do support animations:
		// IE10, FF5-13, Chrome 17<, Safari 4-5.1, Opera 12-12.1
		// Set up a stylesheet that uses animations/keyframes to allow
		// animationStart to see each time a new node loads onto the bodyContent.

		observerStyleSheet =
			document.getElementsByTagName( 'head' )[0].appendChild(
				document.createElement( "style" )
			);
		var oSSText = "@/@-moz-/@-webkit-/@-ms-/@-o-/".split("/")
			.join("keyframes nodeInserted{" +
				"from{outline-color:#fff;}" +
				"to{outline-color:#000;}" +
			"}\n") +
		".mw-content-ltr>*,.mw-content-ltr+*,.visualClear{" +
			// TODO: Fix duplication here.
			"/-moz-/-webkit-/-ms-/-o-/".split("/").join("animation-duration:0.01s;") +
			"/-moz-/-webkit-/-ms-/-o-/".split("/").join("animation-name:nodeInserted;") +
		"}";
		// Pretty much copied from mw.util.
		// Don't have time to wait until it would ordinarily load.
		if( observerStyleSheet.styleSheet ) {
			observerStyleSheet.styleSheet.cssText = oSSText;
		} else {
			observerStyleSheet.appendChild( document.createTextNode( oSSText ) );
		}
		observerStyleSheet = observerStyleSheet.sheet ||
			observerStyleSheet.styleSheet ||
			observerStyleSheet;
		document.addEventListener('animationstart', elemFound, false);
		document.addEventListener('MSAnimationStart', elemFound, false);
		document.addEventListener('webkitAnimationStart', elemFound, false);
	}

	function reactToObserver() {
		if( complete ) {
			return;
		}
		// TODO: Surround main areas in try{} so that if it breaks, at least
		// the content gets dumped back into visibility.

		// First: What part are we up to?
		if( !foundHeader ) {
			// search for header or toc
			if( !recentChild ) { // Just starting, apparently.
				if( !bodyContent ) {
					bodyContent = $(".mw-content-ltr")[0];
					if( !bodyContent ) {
						// bC hasn't loaded yet. Nothing to do here.
						return;
					}
				}
				// Check if we can do the whole thing in one go.
				if( visualClear.length ) {
					removeObserver();
					allAtOnce();
					return;
				}

				recentChild = bodyContent.firstChild;
				if( !recentChild ) {
					return; // Somehow got activated between bodyContent load
					// and bodyContent's content's load.
				}
			}
			for( ; !isHeader( recentChild ) && recentChild.nextSibling; ) {
				recentChild = recentChild.nextSibling;
			}
			if( isHeader( recentChild ) ) { // should simplify check. Maybe !recentChild.nextSibling?
				// We have our first header.
				foundHeader = true;

				checkTab = setUpHashChange();

				// Default to the first tab.
				currentLanguageTab = 0;

				toc = document.getElementById('toc');

				if ( toc ) {
					if ( toc.nextSibling ) {
						// And we have a usable ToC. Makes things much easier.
						// Analyze the ToC. We'll be using it to determine what
						// the tabs are going to be.
						$( ".toclevel-1 > a > .toctext" ).each( function() {
							var language = $( this ).text();
							tabbedLanguages.push( language );
						});

						// Build all the tabs.
						$( tabbedLanguages ).each( newTab );

						toc.parentNode.removeChild( toc );

						// Afterwards, we'll check if the right section
						// is already available.
					} else {
						// Potential panic situation: Evil formatting places
						// ToC *after* the first header, so it's actually
						// *partly* loaded at this point, with no  nextSibling.
						// (Alternatively, someone put it alone in a box.) CSS
						// builds it up as a huge tabbing block, but we can't
						// remove it here. Oy.
						// "Solution":
						// Temporarily hide the ToC until ready() fires, then
						// remove it. I'm not removing it right away, as I have
						// no idea what doom might occur if something like this
						// is removed while stuff is being loaded into it.
						toc.style.display = "none";
						( function ( toc ) {
							$( function() {
								if( toc.parentNode ) {
									toc.parentNode.removeChild( toc );
								}
							});
						})( toc );
						toc = undefined;
						window.console && console.error( "TL notice: " +
							"Malformed entry. ToC either appears after " +
							"headers, or is placed in a box. Mind fixing " +
							"it or alerting me? Thanks. -- YR" );
					}
				}

				languageContainer = languageContainers[ 0 ];

				// Edit buttons.
				processEditButton(
					recentChild.getElementsByClassName( "mw-editsection" )[ 0 ]
				);

				// Either way, start displaying the tabs right.
				bodyContent.insertBefore( tabstable, recentChild );

				if( toc ) {
					tabFound = checkTab();
				}
			}
		}
		if( foundHeader ) {
			// Note that there may or may not be a ToC available...
			// Should this be reworked so that the toc check is inside the loop?
			if( recentChild.nextSibling ) {
				// I hope I'm not going to regret leaving out a recentChild && condition here...
				for( var nextChild; recentChild.nextSibling; ) {
					nextChild = recentChild.nextSibling;
					if( isHeader( recentChild ) ) {
						var langspan = recentChild.getElementsByClassName('mw-headline')[ 0 ];
						var editspan = recentChild.getElementsByClassName('mw-editsection')[ 0 ];
						if( recentHeader >= 0 ) {
							languageContainer.insertBefore( bodyContentFragment, languageContainer.lastChild );
						}
						recentHeader++;

						var language = ( language = langspan ).innerText || language.textContent;

						// Make sure it actually matches, when necessary.
						if( toc && language !== tabbedLanguages[ recentHeader ]) {
							// PANIC!!!
							// Okay, maybe don't panic. TL has encountered
							// a header that doesn't match the ToC's
							// description of the page. Possibilities
							// include an h1 on the page, a fake header
							// around somewhere, or some other malformed
							// kind of header. This really isn't supposed
							// to happen ever.

							// Response: Clear everything after this point,
							// and from there act as though the ToC never
							// existed.
							if( recentHeader > 0 ) {
								toggleLanguageTabs( tabbedLanguages[ 0 ] );
							} else {
								currentLanguageTab = 0;
							}
							while( tabbedLanguages.length > recentHeader ) {
								tabbedLanguages.pop();
								ttr.removeChild(
									languageContainers.pop()
								);
								languageLinks.removeChild(
									languageButtons.pop().parentNode
								);
							}
							toc = undefined; // Never. Existed.
							tabFound = false;

							window.console && console.error( "TL notice: " +
								"Malformed entry. ToC does not match " +
								"headers. Possibly a misplaced H1, fake " +
								"header, or header with incorrect " +
								"contents. Mind fixing it or " +
								"alerting me? Thanks. -- YR "
							);
						}
						if( !toc ) {
							tabbedLanguages.push( language );
							newTab( recentHeader, language );
						}

						// Check to see if the target tab is found.
						// TODO: Need a better system of knowing when to check:
						//   If it's found, stop checking.
						//   Whether or not we have a ToC, checking can be necessary.
						//   If we do have a ToC, but no hash, we don't check, right?
						//   Put another way:
						//     No hash? W/ ToC, don't check. W/o ToC, only check when new headers come in.
						//     Hash? Depends.
						//       W/ ToC: Technically, target could come in at any non-header element... ???
						//       W/o ToC: Could be whenever. Target header coming in is rather likely, in fact. Oy.
						//       Either way: If the target, whether header or random element, is found, stop looking.
						//     Does delaying the tab switch to a element target until full load matter? Not sure.
						//
						//	Plan B: Regardless of ToC presence, only
						//	check for potential target elems
						//	at each new header, unless target is found.
						if( recentHeader > 0 && tabFound === false ) {
							tabFound = checkTab();
						}
						languageContainer = languageContainers[ recentHeader ];
						if( recentHeader > 0 ) { // I have too many of these checks...
							// Don't duplicate earlier pEB for first header.
							processEditButton( editspan );
						}
						// All done. Clear.
						bodyContent.removeChild( recentChild );
					} else {
						if( recentChild.nodeName === "HR" ) {
							// Kill unnecessary bars.
							bodyContent.removeChild( recentChild );
						} else {
							// Regular content. Prepare for dumping into the
							// latest tab.
							bodyContentFragment.appendChild( recentChild );
						}
					}
					recentChild = nextChild;
				}
			}
			/*
			// If we're all done, finish up.
			// How to tell?
			// Option one: Repeatedly check for nextSibling.
			// 	 - Won't work. bodyContent doesn't always have a nS at the end.
			//   - Currently in use only for dealing with the last elem. (Uses O2 as fallback.)
			// Option two: Set up getElemsByClassName, and poll for length.
			//	 - presumably visualClear. This would probably be pretty heavy.
			//   - Currently in use.
			// Option three: Use $.ready.
			//	 - Potentially introduces substantial delay...
			// Option four: In the listener, check for classnames each time.
			//	 - Every time there's a new node? Lots of processing.
			// Option five: ...
			*/
			if( bodyContent.nextSibling && bodyContent.lastChild === recentChild ) { // are we done? ...
				// bodyContent.nextSibling doesn't always exist, but if it
				// does, get a head start on the stuff that can be done already.
				bodyContentFragment.appendChild( bodyContent.lastChild );
				/*
				// Problem: recentChild is now inside the languageContainer,
				// and it's possible that reactToObserver will be called again
				// before the post-visualClear stuff runs.
				// Option one: recentChild = tabstable;
				// - Nope. That would mean bodyContent.lastChild = recentChild again
				// Option two: recentChild = false;
				// - Nope. The check at the top would cause us to start over again.
				// Blargh. This is an awful solution that probably violates every...
				*/
				recentChild = 1;
				// "1" is hereby the indicator that we're not doing anything with
				// this particular variable, okay?
				// I'll try to make this nicer later.
			}

			// Dump collected elems into most recent languageContainer.
			if( bodyContentFragment.firstChild ) { // Exact duplicate of above code. TODO: Fix.
				languageContainer.insertBefore( bodyContentFragment, languageContainer.lastChild );
			}

			if( visualClear.length ) {
				complete = true;
				if( bodyContent.lastChild === recentChild ) {
					// bC.nS didn't exist. Move the last one left now.
					languageContainer.insertBefore( bodyContent.lastChild, languageContainer.lastChild );
				}
				removeObserver();
				sortCats();
				if( tabFound !== true ) {
					checkTab();
					if( location.hash === '' ) {
						location.replace( "#" + tabbedLanguages[currentLanguageTab] );
					}
				}
			}
		}
	}
	function removeObserver() {
		if( observer ) {
			observer.disconnect();
		} else if( observerStyleSheet ) {
			document.removeEventListener('animationstart', elemFound, false);
			document.removeEventListener('MSAnimationStart', elemFound, false);
			document.removeEventListener('webkitAnimationStart', elemFound, false);
			observerStyleSheet.disabled = true;
		}
		if( timer !== false ) {
			clearTimeout( timer );
		}
	}
}

function isHeader( elem ) {
	return elem && elem.nodeName === "H2" && elem.getElementsByClassName( "mw-headline" ).length !== 0;
}

function newTab( index, language ) {
	var active = index === currentLanguageTab;
	var languageContainer = ttr.appendChild( newNode('td', {
		'class': 'languageContainer',
		'id': language + 'container'
	}, active ? undefined : { 'style' : 'display:none;' } ));
	languageContainers.push( languageContainer );
	newCategoryBox( languageContainer, language );
	// lB contains .(un)?selectedTab nodes
	languageButtons.push(
		languageLinks.appendChild( newNode('tr', newNode('td', {
			'class': ( active ? '' : 'un' ) + 'selectedTab'
		}, newNode('a', language, {
			// Note: ' + language' makes this inconsistent with the ordinary links
			// themselves, which have the language name encoded. Issue?
			// Keep in mind that the parser itself actually corrects for this,
			// changing #!Xóõ links to the proper #.C7.83X.C3.B3.C3.B5 links.
			// Probably nothing to worry about.
			'href': location.pathname + location.search + '#' + language
		}), ' '))).firstChild
	);
}

function newCategoryBox( container, name ) {
	// Put a container in each for categories.
	return container.appendChild( newNode('div', name + ' categories: ', newNode('ul'), {
		'class': 'catlinks',
		'id': 'catlinks'
	}));
}

function processEditButton( button ) {
	if( button ) {
		// Yes, theoretically if you have some fake H2s the edit buttons will
		// be misplaced. Yet another TODO...
		var len = languageHeaderEditButtons.push( button );
		if( len === 1 ) {
			caption = document.createElement('caption');
			tabstable.insertBefore( caption, tabstable.firstChild );
		}
		button.className += " editlangsection";
		// use a argument or variable ( from .push? ) instead of .length?
		if( len - 1 === currentLanguageTab ) {
			caption.firstChild && caption.removeChild( caption.firstChild );
			caption.appendChild( button );
		}
	}
}

// Sets up the hash toggle system.
// Returns the checkTab() function, which returns true if
// we found whatever it was.
function setUpHashChange() {

	// Important Note: The decodeURI mess is a real mess.
	// #.C7.83X.C3.B3.C3.B5 should go to tab "!Xóõ", with
	// the id "!Xóõcontainer" (no encoding). Urgh.
	// Also, "#Old_English" needs to go to decoded "Old English".

	// Remember to .substr( 1 ) before passing here.
	function decodeHash( hash ) {
		return decodeURI(
			hash
				.replace(/\.(?=[0-9A-F]{2})/g, '%')
				.replace(/_/g, ' ')
		);
	}

	// Called by onhashchange.
	function hashToggleLT() {
		var destination = decodeHash( location.hash.substr(1) );
		toggleLanguageTabs( destination );
		tabbedLanguages[currentLanguageTab] !== destination && resetHash();
	}

	// For updating page positioning.
	// Doesn't activate hashchange, at least in Chrome. Not sure about others.
	function resetHash() {
		location.replace( location.hash );
	}

	// Need to decide if language arg is encoded or decoded.
	// Will work with either for the moment. Decoded works earlier.
	// Currently passed as decoded by every function but itself.
	// The hashes are encoded, but hashToggleLT decodes them.

	// Toggles to a different language tab.
	window.toggleLanguageTabs = function (language) {
		// Find the destination language.

		var destinationLanguageTab = $.inArray( language, tabbedLanguages );

		if( destinationLanguageTab === -1 ) {
			var decoded = decodeHash( language );
			if( decoded !== language ) {
				destinationLanguageTab = $.inArray( decoded, tabbedLanguages );
			}
		}

		// Style the right toggle button, hide the old language section and show the new one.

		// var languageButtons = $("#languageLinks .selectedTab, #languageLinks .unselectedTab");
		if (destinationLanguageTab !== -1 ) {
			if( destinationLanguageTab !== currentLanguageTab ) {
				languageButtons[currentLanguageTab].className = 'unselectedTab';
				languageContainers[currentLanguageTab].style.display = 'none';
				currentLanguageTab = destinationLanguageTab;
				languageButtons[destinationLanguageTab].className = 'selectedTab';
				languageContainers[destinationLanguageTab].style.display = '';
				if (caption) {
					// extra checks shouldn't be necessary...
					caption.firstChild && caption.removeChild(caption.firstChild);
					languageHeaderEditButtons[currentLanguageTab] && caption.appendChild(languageHeaderEditButtons[currentLanguageTab]);
				}
			}
		} else {
			// Does the hash match the id of a node in a tab?
			language = encodeURI( language.replace(/\ /g, '_') );
			// Yes, I just possibly undid the decoding from hashToggleLT.
			// Worse, this might double-encode, breaking things.
			// Or maybe not? I think everything might be decoded before being
			// sent here?
			// I'll deal with it later.

			// Find the node, and go up the node tree until
			// you hit .languageContainer, or nothing.
			language = document.getElementById( language );
			for( ; language && ( language = language.parentNode ) &&
				language.className !== 'languageContainer'; )
				;
			// language = language && language.parentNode.parentNode;
			if ( language ) {
				// If someone maliciously makes a languageContainer with a
				// non-compliant ID, boom.
				toggleLanguageTabs( language.id.split('container')[0] );
			}
			// Possible doom bug: Endless loop?
		}
	};

	// This function gets returned as checkTab(), btw.
	// For during or immediately after load: Check if we have a
	// good "starting" tab. Return true if we have a definitive find.
	function checkTab() {
		// If there's a location hash, the window may have scrolled down before
		// we got a chance to reorganize everything.
		// If the destination was a subsection or sense id, switch to the right
		// tab, and rescroll.
		// If it was simply a language, switch to the appropriate tab, and
		// scroll back up.
		// If there's no hash at all, work off the TargetedTranslations prefs.
		try {
			var hash = '';
			var destination = 'English';

			if ( location.hash !== '' ) {
				hash = ( location.hash ).substr( 1 ); // does hash.substr always exist?
				destination = decodeHash( hash );
			}

			// 'k, this is still awful.
			if ( $.inArray( destination, tabbedLanguages ) !== -1 ) {
				toggleLanguageTabs( destination );
				window.scrollY && window.scroll(0, 0);
				return true;
			} else if ( hash !== '' && document.getElementById( hash ) ) {
				// This was going
				// to receive an area to search from an argument, but it turns
				// out that elem.getElementById doesn't actually exist. Meh.
				toggleLanguageTabs( destination );
				resetHash(); // Scroll to the element. (Necessary since we
				// this isn't running through hashToggleLT, which normally does
				// it in these situations.)
				return true;
			}

			// No hash. Work from TargetedTranslations.
			if ('localStorage' in window) {
				if (tabbedLanguages[0] !== 'Translingual' && tabbedLanguages[0] !== 'English' && localStorage.TargetedTranslations) {
					for (
						var tt_ = localStorage.TargetedTranslations.split("|"), tt = tt_[0].split(";").concat(
							$.grep(
								tt_[1].replace(/[^;\/]+\//g, '').split(";"),
								function (z) {
									return z && z !== "Latin" && z !== "Hebrew" && z !== "Arabic";
								}
							)
						).concat( tt_[1].replace(/\/[^;]+/g, '').split(";") ), i = 0;
						i < tt.length;
						i++
					) {
						if ($.inArray(tt[i], tabbedLanguages) !== -1) {
							toggleLanguageTabs(tabbedLanguages[$.inArray(tt[i], tabbedLanguages)]);
							break;
						}
					}
				}
			}

			return false;
		} catch ( e ) {
			// This probably isn't all that unlikely to happen. Too complicated. :(
			window.console && console.error( "TL error: checkTab broke.", e );
		}
	}
	if ("onhashchange" in window && (document.documentMode === undefined || document.documentMode > 7)) {
		window.onhashchange = hashToggleLT;
	} else {
		$( bodyContent ).on(
			'click',
			'a[href^="' + location.pathname + location.search + '#"], a[href^="#"]',
			function () {
				setTimeout( hashToggleLT, 10 );
			}
		);
	}

	return checkTab;
}

function sortCats() {
	var catDiv = document.getElementById('mw-normal-catlinks'),
		currentCatDiv;
	if (catDiv) {
		var cats = catDiv.getElementsByTagName('li'),
			catname,
			langcurrent = 0,
			catskip = 1;
		do {
			while (cats.length > 0) {
				catname = cats[ 0 ].getElementsByTagName('a')[0].innerHTML;
				if (catname.indexOf(tabbedLanguages[langcurrent + catskip]) === 0 &&
					!/letter\snames$|script\scharacters$|mythology$/.test(catname)
				) {
					langcurrent += catskip;
					catskip = 1;
				}
				currentCatDiv = languageContainers[langcurrent].lastChild;
				currentCatDiv.lastChild.appendChild(cats[0]);
			}
			if (langcurrent + 1 < languageContainers.length - catskip) {
				// Didn't make it to the end, which means there's a section
				// without any categories. Dump what's left back and try again.
				while (currentCatDiv.lastChild.firstChild) {
					catDiv.lastChild.appendChild(currentCatDiv.lastChild.firstChild);
				}
				catskip++;
			} else {
				break;
			}
		} while (true);
	}

	if( currentCatDiv ) {
		// place patrol link at the bottom of the page
		var pl = currentCatDiv.previousSibling; // languageContainer.lastChild.previousSibling;
		// Got it, the patrollink is sometimes the lagging last elem.
		if( pl && pl.className === "patrollink" ) {
			tabstable.parentNode.appendChild(pl);
		}
	}
	/*
	// category editing buttons
	if (mw.config.get('wgAction') === "view" && !/&printable=yes|&diff=|&oldid=/.test(location.search)) {
		for (z = 0; z < languageContainers.length; z++) {
			// TODO: import
			// addTabbedLanguageNewCatButton(z);
		}
		if ( window.loadremovecatbuttons === true ) {
			$.get(
				mw.config.get( 'wgScript' ),
				{ 'title' : mw.config.get('wgPageName'), 'action' : 'raw' },
				// Note that this isn't actually defined. I'm considering not
				// actually importing the function, and just dumping this section.
				addRemoveCatButtons
			);
		}
	}
	*/
	// Remove old cat box, allow display of hidden cats box.
	if (catDiv && !(catDiv.nextSibling && catDiv.nextSibling.className === "mw-hidden-catlinks mw-hidden-cats-user-shown")) {
		catDiv.parentNode.style.display = 'none';
	} else {
		if (catDiv) {
			catDiv.style.display = 'none';
		}
	}
}


function testStuff() {
	var delay = 5000, f = document.createDocumentFragment();
	var x = [
		function () {
			// $("#toc").remove();
			var x = document.getElementById( "mw-content-text" );
			while( x.nextSibling ) {
				f.appendChild( x.nextSibling );
			}
		}, function () {
			var y = $( ".mw-content-ltr" )[ 0 ];
			var q = document.createDocumentFragment();
			while( y.firstChild ) {
				q.appendChild( y.firstChild );
			}
			function u( i, e ) {
				setTimeout( function () {
					y.appendChild( q.firstChild );
				}, i / e * delay );
			}
			for( var i = 0, e = q.childNodes.length; i < e; i++ ) {
				u( i, e ); // encapsulate
			}
			makeTabsfromScratch();
		}, function () {
			function u( i, e ) {
				setTimeout( function () {
					$(".mw-content-ltr").after( f.firstChild );
				}, i / e * delay / 5 );
			}
			for( var i = 0, e = f.childNodes.length; i < e; i++ ) {
				u( i, e ); // encapsulate
			}
			console.log(9);
		}
	];

	x[0]();
	for( var i = 1; i < x.length; i++ ) {
		setTimeout( x[ i ], (i-1) * delay + 50 );
	}
}

//testStuff();
makeTabsfromScratch();

// This is officially deprecated as of MW1.22. Should be replaced by mw.hook,
// but the docs don't say what event, so...
$(mw).on('LivePreviewDone', function () {
	bodyContent = $(".mw-content-ltr")[0]; // reset
	makeTabsfromScratch();
});

})( window.jQuery );