Benutzer:P.Copp/scripts/templateutil.js

aus Wikipedia, der freien Enzyklopädie
Zur Navigation springen Zur Suche springen

Hinweis: Leere nach dem Veröffentlichen den Browser-Cache, um die Änderungen sehen zu können.

  • Firefox/Safari: Umschalttaste drücken und gleichzeitig Aktualisieren anklicken oder entweder Strg+F5 oder Strg+R (⌘+R auf dem Mac) drücken
  • Google Chrome: Umschalttaste+Strg+R (⌘+Umschalttaste+R auf dem Mac) drücken
  • Internet Explorer/Edge: Strg+F5 drücken oder Strg drücken und gleichzeitig Aktualisieren anklicken
  • Opera: Strg+F5
/**************************************************************************************************
* templateutil.js
*/

// <nowiki>

/**************************************************************************************************
 * Messages
 */

if( mw.config.get('wgUserLanguage') === 'de' ) {
	mw.messages.set( {
		'tplutil-button-changes'      : 'Änderungen zeigen',
		'tplutil-button-discard'      : 'Schließen',
		'tplutil-button-edit'         : 'Vorlage bearbeiten...',
		'tplutil-button-reload'       : 'Neu laden',
		'tplutil-button-save'         : 'Speichern...',
		'tplutil-button-title'        : 'Titel ändern...',
		'tplutil-categories'          : 'Kategorien: ',
		'tplutil-confirm-discard'     : 'Achtung: Alle Änderungen an der Vorlage gehen verloren.'
		                                + ' Vorlage schließen?',
		'tplutil-confirm-reload'      : 'Achtung: Alle Änderungen an der Vorlage gehen verloren.'
		                                + ' Text neu laden?',
		'tplutil-dialog-changes'      : 'Änderungen an "$1"',
		'tplutil-prompt-summary'      : 'Zusammenfassung der Änderung:',
		'tplutil-prompt-title'        : 'Neuer Titel:',
		'tplutil-report-header'       : '<tr><th rowspan=2>Name</th><th rowspan=2>Aufrufe</th>'
		                                + '<th colspan=2>Lastanteil gesamt</th><th colspan=2>'
		                                + 'Lastanteil selbst</th></tr><tr><th>(ms)</th><th>(%)'
		                                + '</th><th>(ms)</th><th>(%)</th></tr>',
		'tplutil-report-row'          : '<tr><td style="text-align:left">$1$2</td><td>$3</td><td>'
		                                + '$4</td><td>$5</td><td>$6</td><td>$7</td></tr>',
		'tplutil-result-editconflict' : 'Bearbeitungskonflikt',
		'tplutil-result-noconnect'    : 'Verbindungsfehler',
		'tplutil-result-other'        : 'Ergebnis des Speicherns: $1',
		'tplutil-result-protected'    : 'Die Seite ist für das Bearbeiten gesperrt',
		'tplutil-result-success'      : 'Speichern erfolgreich',
		'tplutil-result-unknown'      : 'Fehler beim Bearbeiten: $1',
		'tplutil-spinner-url'         : 'http://upload.wikimedia.org/wikipedia/commons/f/f8/'
		                                + 'Ajax-loader(2).gif',
		'tplutil-status-loaddeps'     : 'Lade Vorlagen... ',
		'tplutil-status-preview'      : 'Lade Vorschau... ',
		'tplutil-status-report'       : 'Erstelle Bericht... ',
		'tplutil-status-tree'         : 'Erstelle Baumstruktur... ',
		'tplutil-tab-edit'            : 'Bearbeiten',
		'tplutil-tab-preview'         : 'Vorschau',
		'tplutil-tab-report'          : 'Vorlagenbericht',
		'tplutil-tab-tree'            : 'Vorlagenbaum',
		'tplutil-tb-show'             : 'Vorlagenwerkzeuge',
		'tplutil-tb-tooltip'          : 'Blendet Hilfsmittel für das Bearbeiten von Vorlagen ein',
		'tplutil-tree-close'          : 'X',
		'tplutil-tree-edit'           : 'Edit',
		'tplutil-tree-empty'          : '<!-- leer -->'
	} );
} else {
	mw.messages.set( {
		'tplutil-button-changes'      : 'Show changes',
		'tplutil-button-discard'      : 'Discard',
		'tplutil-button-edit'         : 'Edit template...',
		'tplutil-button-reload'       : 'Reload',
		'tplutil-button-save'         : 'Save...',
		'tplutil-button-title'        : 'Change title...',
		'tplutil-categories'          : 'Categories: ',
		'tplutil-confirm-discard'     : 'All changes to this template are lost. Discard?',
		'tplutil-confirm-reload'      : 'All changes to this template are loadt. Reload?',
		'tplutil-dialog-changes'      : 'Changes to "$1"',
		'tplutil-prompt-summary'      : 'Summary:',
		'tplutil-prompt-title'        : 'New title:',
		'tplutil-report-header'       : '<tr><th rowspan=2>Name</th><th rowspan=2>Calls</th>'
		                                + '<th colspan=2>Load total</th><th colspan=2>Load own'
		                                + '</th></tr><tr><th>(ms)</th><th>(%)</th><th>(ms)</th>'
		                                + '<th>(%)</th></tr>',
		'tplutil-report-row'          : '<tr><td style="text-align:left">$1$2</td><td>$3</td><td>'
		                                + '$4</td><td>$5</td><td>$6</td><td>$7</td></tr>',
		'tplutil-result-editconflict' : 'Edit conflict',
		'tplutil-result-noconnect'    : 'Connection error',
		'tplutil-result-other'        : 'Save result: $1',
		'tplutil-result-protected'    : 'This page is protected.',
		'tplutil-result-success'      : 'Save successful',
		'tplutil-result-unknown'      : 'Save error: $1',
		'tplutil-spinner-url'         : 'http://upload.wikimedia.org/wikipedia/commons/f/f8/'
		                                + 'Ajax-loader(2).gif',
		'tplutil-status-loaddeps'     : 'Loading templates... ',
		'tplutil-status-preview'      : 'Loading preview... ',
		'tplutil-status-report'       : 'Generating report... ',
		'tplutil-status-tree'         : 'Generating expansion tree... ',
		'tplutil-tab-edit'            : 'Edit',
		'tplutil-tab-preview'         : 'Preview',
		'tplutil-tab-report'          : 'Report',
		'tplutil-tab-tree'            : 'Expansion tree',
		'tplutil-tb-show'             : 'Template tools',
		'tplutil-tb-tooltip'          : 'Shows tools for editing and debugging templates',
		'tplutil-tree-close'          : 'X',
		'tplutil-tree-edit'           : 'Edit',
		'tplutil-tree-empty'          : '<!-- empty -->'
	} );
}

/**************************************************************************************************
 * UI
 */

(function() {
	var has = Object.prototype.hasOwnProperty;
	var templates = {};
	var selected;
	var contextTitle;
	var unchanged = {};
	var textarea, tbLink, $select, $content, $discardButton;

	$( function() {
		tbLink = mw.util.addPortletLink( 'p-tb', '#', m( 'tb-show' ), '', m( 'tb-tooltip' ) );
		if(tbLink) tbLink.onclick = init;
	} );
	function init() {
		$( tbLink ).remove();
		importStylesheetURI( 'http://de.wikipedia.org/w/index.php?action=raw&title=Benutzer:P.Copp/scripts/templateutil.css&ctype=text/css&maxage=21600' );
		importStylesheetURI( stylepath + '/common/diff.css' );
		mw.loader.load( 'jquery.ui' );
		var script = mw.loader.load( '//de.wikipedia.org/w/index.php?action=raw&title=Benutzer:P.Copp/scripts/parser.js&ctype=text/javascript&maxage=21600' );
		var done = false;
		if( script ) {
			script.onload = script.onreadystatechange = function() {
				var state = this.readyState;
				if( !done && ( !state || state === 'loaded' || state === 'complete' ) ) {
					done = true;
					showUI();
				}
			};
		}
		return false;
	}
	function showUI() {
		CustomExpander.prototype = $.extend(
			new mw.parser.TemplateExpander,
			CustomExpander.prototype
		);
		ReportExpander.prototype = $.extend( new CustomExpander, ReportExpander.prototype );
		TreeExpander.prototype = $.extend( new CustomExpander, TreeExpander.prototype );
		$( '#p-views' ).hide();
		$( '#p-namespaces, #p-cactions' ).find( 'li' ).hide();
		var portlet = skin === 'vector' ? 'p-namespaces' : 'p-cactions';
		var contentId = skin === 'modern' ? 'mw_content' : 'content';
		$content = $( '<div>' ).attr( 'id', contentId );
		$.each( ['edit', 'preview', 'tree', 'report'], function( k, v ) {
			mw.util.addPortletLink(
				portlet,
				'#',
				m( 'tab-' + v ),
				'tplutil-tab-' + v,
				m( 'tab-tooltip-' + v )
			);
			$content.append( $( '<div>' ).attr( 'id', 'tplutil-div-' + v ) );
		} );
		$content.append( '<div class="visualClear" />' );
		$( '#' + portlet ).delegate( 'li', 'click', onTabClick );
		$( '#' + contentId ).replaceWith( $content );
		$select = $( '<select size="30">' ).change( onSelectChange );
		$( '<div id="tplutil-div-select">' )
			.append( $( '<button>' ).text( m( 'button-edit' ) ).click( promptEdit ) )
			.append( $select )
			.appendTo( $( '#tplutil-div-edit' ) );
		textarea = $( '<textarea>' )[0];
		$discardButton = $( '<button>' ).text( m( 'button-discard' ) ).click( confirmDiscard );
		$( '<div id="tplutil-div-area">' )
			.append( '<h4>&nbsp;</h4>' )
			.append( textarea )
			.append( $( '<button>' ).text( m( 'button-save' ) ).click( promptSummary ) )
			.append( $( '<button>' ).text( m( 'button-title' ) ).click( promptTitle ) )
			.append( $( '<button>' ).text( m( 'button-reload' ) ).click( confirmReload ) )
			.append( $( '<button>' ).text( m( 'button-changes' ) ).click( loadChanges ) )
			.append( $discardButton )
			.appendTo( $( '#tplutil-div-edit' ) );
		addTemplate( mw.config.get('wgPageName') );
		$( '#tplutil-tab-edit' ).click();
		$select.change();
	}

	/**********************************************************************************************
	 * Edit
	 */
	function saveCurrentText() {
		if( selected && textarea.value !== templates[selected] ) {
			templates[selected] = textarea.value;
			unchanged = {};
		}
	}
	function addTemplate( title ) {
		var page = mw.parser.getPage( title );
		if( !page ) {
			return;
		}
		var title = mw.api.cache.getTarget( page.ptitle );
		if( has.call( templates, title ) ) {
			$select.val( title ).change();
		} else {
			var $opt = $( '<option/>' ).val( title ).text( title );
			if( !contextTitle ) {
				contextTitle = title;
			}
			loadTemplate( title );
			$select.append( $opt ).val( title ).change();
		}
	}
	function loadTemplate( title ) {
		mw.api.cache.getItemNow( 'text', title, function( text ) {
			templates[title] = text;
			if( selected === title ) {
				textarea.value = text || '';
			}
			var newTitle = mw.api.cache.getTarget( title );
			if( newTitle !== title ) {
				if( has.call( templates, newTitle ) ) {
					delTemplate( title );
					$select.val( newTitle ).change();
				} else {
					changeTitle( title, newTitle );
				}
			}
		} );
	}
	function changeTitle( old, newTitle ) {
		if( old !== newTitle ) {
			templates[newTitle] = templates[old];
			delete templates[old];
			if( old === selected ) {
				selected = newTitle;
			}
			if( old === contextTitle ) {
				contextTitle = newTitle;
			}
			$select.find( 'option' ).each( function() {
				if( this.value === old ) {
					$( this ).val( newTitle ).text( newTitle );
					return false;
				}
			} );
			$select.change();
			unchanged = {};
		}
	}
	function delTemplate( title ) {
		$select.find( 'option' ).each( function() {
			if( this.value === title ) {
				$( this ).remove();
			}
		} );
		if( selected === title ) {
			$select.val( contextTitle ).change();
		}
		delete templates[title];
	}
	function onSelectChange() {
		saveCurrentText();
		var title = this.value;
		textarea.value = templates[title] || '';
		$( '#tplutil-div-area' ).find( 'h4' ).text( title );
		selected = title;
		$discardButton.toggle( title !== contextTitle );
	}
	function onTabClick() {
		$( this ).addClass( 'selected' ).siblings().removeClass( 'selected' );
		$content.children( ':not(.visualClear)' ).hide();
		var tab = this.id.substring( 'tplutil-tab-'.length );
		$( '#tplutil-div-' + tab ).show();
		if( tab !== 'edit' ) {
			saveCurrentText();
			if( !unchanged[tab] ) {
				unchanged[tab] = true;
				loadTemplates( tab );
			}
		}
		return false;
	}
	function promptEdit() {
		addTemplate( prompt( m( 'prompt-title' ), mw.config.get('wgFormattedNamespaces')[10] + ':' ) );
	}
	function promptSummary() {
		var summary = prompt( m( 'prompt-summary' ) );
		if( summary === null ) {
			return;
		}
		var pi = mw.api.cache.getItem( 'page', selected );
		mw.api.call( {
			action  : 'edit',
			title   : selected,
			token   : mw.api.cache.getItem( 'token' ),
			summary : summary || '',
			basetimestamp : pi && pi.revisions[0].timestamp,
			text    : textarea.value
		}, function( res ) {
			if( !res ) {
				alert( m( 'result-noconnect' ) );
			} else if( res.error && res.error.code === 'editconflict' ) {
				alert( m( 'result-editconflict' ) );
			} else if( res.edit && res.edit.result === 'Success' ) {
				alert( m( 'result-success' ) );
			} else if( res.error && res.error.code === 'protectedpage' ) {
				alert( m( 'result-protected' ) );
			} else if( res.error ) {
				alert( m( 'result-unknown', res.error.info ) );
			} else {
				alert( m( 'result-other', res.edit && res.edit.result ) );
			}
		} );
	}
	function promptTitle() {
		var title = prompt( m( 'prompt-title' ), selected );
		if( title ) {
			changeTitle( selected, title );
		}
	}
	function confirmReload() {
		if( confirm( m( 'confirm-reload' ) ) ) {
			loadTemplate( selected );
		}
	}
	function loadChanges() {
		var title = selected;
		mw.api.call( {
			action : 'query',
			prop   : 'revisions',
			rvprop : '',
			titles : title,
			rvdifftotext : textarea.value
		}, function( res ) {
			var p = res.query.pages;
			for( var i in p ) if( has.call( p, i ) ) {
				if( p[i].revisions ) {
					$( '<table class="diff">' ).html( '<col class="diff-marker" />'
						+ '<col class="diff-content" /><col class="diff-marker" />'
						+ '<col class="diff-content" />' + p[i].revisions[0].diff['*'] )
						.dialog( {
							title : mw.html.escape( m( 'dialog-changes', title ) ),
							width : '75%'
						} );
				}
				return;
			}
		} );
	}
	function confirmDiscard() {
		if( confirm( m( 'confirm-discard' ) ) ) {
			delTemplate( selected );
		}
	}
	function showSelection( title, obj ) {
		addTemplate( title );
		$( '#tplutil-tab-edit' ).click();
		var ta = textarea;
		ta.focus();
		if( ta.value.substr( obj.offset, obj.len ) !== mw.parser.expandWiki( obj ) ) {
			return;
		}
		if( ta.selectionStart !== undefined ) {
			//TODO: This doesn't work in Opera due to some strange handling of newlines
			ta.setSelectionRange( obj.offset, obj.offset + obj.len );
			ta.scrollTop = getTextHeight( ta, ta.value.substring( 0, obj.offset ) ) - 30;
		} else if( ta.createTextRange ) {
			var range = ta.createTextRange();
			range.move( 'character', obj.offset );
			range.moveEnd( 'character', obj.len );
			range.select();
		}
	}

	/**********************************************************************************************
	 * Preview
	 */
	function createPreview() {
		mw.api.call( {
			action : 'parse',
			prop   : 'text|categories',
			title  : contextTitle,
			text   : '__NOEDITSECTION__' + expand( CustomExpander )
		}, showPreview );
	}
	function showPreview( res ) {
		var html = '<div><h1>' + mw.html.escape( contextTitle ) + '</h1>' + res.parse.text['*'];
		if( res.parse.categories.length ) {
			html += '<div class="catlinks">' + mw.html.escape( m( 'categories' ) );
			$.each( res.parse.categories, function( i, cat ) {
				if( i > 0 ) {
					html += ' | ';
				}
				var page = mw.parser.getPage( 'Category:' + cat['*'] );
				html += '<a href="' + mw.html.escape( ( new CustomExpander ).getLocalUrl( page ) )
				        + '" title="' + mw.html.escape( page.ptitle ) + '">'
				        + mw.html.escape( page.title ) + '</a>';
			} );
			html += '</div>';
		}
		html += '</div>';
		$( '#tplutil-div-preview' ).html( html );
	}

	/**********************************************************************************************
	 * Report
	 */
	var reportResult;
	function ReportNode( name, parent ) {
		this.name = name;
		this.parent = parent;
		this.total = 0;
		this.own = 0;
		this.num = 0;
		this.calls = {};
	}
	function ReportExpander() {
		CustomExpander.apply( this, arguments );
		reportResult = this.reportNode = new ReportNode( contextTitle );
	}
	ReportExpander.prototype.expandTemplatePage = function( obj, page ) {
		var title = this.getRedirectTarget( page ) || page.ptitle;
		if( !has.call( this.reportNode.calls, title ) ) {
			this.reportNode.calls[title] = new ReportNode( title, this.reportNode );
		}
		this.reportNode = this.reportNode.calls[title];
		this.reportNode.num++;
		var result = CustomExpander.prototype.expandTemplatePage.apply( this, arguments );
		this.reportNode = this.reportNode.parent;
		return result;
	};
	ReportExpander.prototype.expandPart = function( part, side ) {
		for(
			var node = this.reportNode;
			this.params && node.name !== this.params.title;
			node = node.parent
		);
		node.own++;
		for( ; node; node = node.parent ) {
			node.total++;
		}
		return CustomExpander.prototype.expandPart.apply( this, arguments );
	};

	function createReport() {
		expand( ReportExpander );
		$( '#tplutil-div-report' ).html(
			'<table class="wikitable">' + m( 'report-header' )
			+ createReportRows( reportResult, 1, reportResult.total ) + '</table>'
		);
	}
	function createReportRows( node, level, total ) {
		var indent = ( new Array( level ) ).join( '&nbsp;&nbsp;&nbsp;' );
		var rows = m( 'report-row', indent, node.name, node.num,
			( node.total / 5 ).toFixed( 0 ), ( node.total * 100 / total ).toFixed( 2 ),
			( node.own / 5 ).toFixed( 0 ), ( node.own * 100 / total ).toFixed( 2 )
		);
		var sorted = [];
		for( var i in node.calls ) if( has.call( node.calls, i ) ) {
			sorted.push( node.calls[i] );
		}
		sorted.sort( function( a, b ) { return b.total - a.total; } );
		for( var i = 0; i < sorted.length; i++ ) {
			rows += createReportRows( sorted[i], level + 1, total );
		}
		return rows;
	}

	/**********************************************************************************************
	 * Expansion Tree
	 */
	function TreeNode( obj, title ) {
		this.obj = obj;
		this.title = title || contextTitle;
		this.parts = [];
		this.cur = el( 'div', 'body' );
	}
	TreeNode.prototype.show = function() {
		if( !this.block ) this.createBlock();
		this.small.parentNode.replaceChild( this.block, this.small );
	};
	TreeNode.prototype.hide = function() {
		this.block.parentNode.replaceChild( this.small, this.block );
	};
	TreeNode.prototype.createPart = function( num, side ) {
		if( !this.parts[num] ) {
			this.parts[num] = el( 'tr', 'part', el( 'td', 'unexpanded' ) );
		}
		if( side === 'l' && this.parts[num].childNodes.length < 2 ) {
			this.parts[num].insertBefore( el( 'td' ), this.parts[num].firstChild );
		}
		if( side !== 'l' ) {
			this.parts[num].lastChild.className = '';
		}
		return side === 'l' ? this.parts[num].firstChild : this.parts[num].lastChild;
	};
	TreeNode.prototype.createBlock = function() {
		var title = this.title, obj = this.obj;
		this.block = el( 'div', 'block-' + this.type );
		var table = el( 'table', 'args' );
		for( var i = 0, num = 0; i < this.obj.parts.length; i++ ) {
			if( !this.parts[i] ) {
				this.createPart( i, 'both' );
				this.parts[i].lastChild.className = 'tplutil-unexpanded';
			}
			if( this.parts[i].lastChild.className === 'tplutil-unexpanded' ) {
				var text = this.parts[i].childNodes.length < 2
					? mw.parser.Preprocessor.prototype.expandPart( this.obj.parts[i] )
					: mw.parser.Preprocessor.prototype.expandPart( this.obj.parts[i], 'r' );
				this.parts[i].lastChild.appendChild(
					document.createTextNode( text.trim() || m( 'tree-empty' ) )
				);
			}

			if( i > 0 && this.type === 'template' && this.parts[i].childNodes.length < 2 ) {
				this.parts[i].insertBefore(
					el( 'td', 'numbered', ++num ),
					this.parts[i].firstChild
				);
			} else if( this.parts[i].childNodes.length < 2 ) {
				this.parts[i].firstChild.colSpan = 2;
			}

			if( i === 0 ) {
				if( this.type === 'template' ) {
					this.parts[i].lastChild.className = 'tplutil-part-title';
				}
				this.parts[i].lastChild.insertBefore(
					el(
						'a',
						'xtoggle',
						m( 'tree-edit' ),
						function() { showSelection( title, obj ); }
					),
					this.parts[i].lastChild.firstChild
				);
				this.parts[i].lastChild.insertBefore(
					el( 'a', 'xtoggle', m( 'tree-close' ), func( this, 'hide' ) ),
					this.parts[i].lastChild.firstChild
				);
			}
			table.appendChild( this.parts[i] );
		}
		this.block.appendChild( table );
		if( this.body ) {
			this.block.appendChild( this.body );
		}
	};
	TreeNode.prototype.createResult = function( result ) {
		this.small = el( 'span', 'small-' + this.type, result || m( 'tree-empty' ) );
		this.small.onclick = func( this, 'show' );
		this.small.title = mw.parser.expandWiki( this.obj );
		return this.small;
	};

	var treeResult;
	function TreeExpander() {
		CustomExpander.apply( this, arguments );
		treeResult = this;
	}
	TreeExpander.prototype.root = function( obj ) {
		var title = this.params && this.params.title || contextTitle;
		this.node = obj.node = this.node || new TreeNode( obj, title );
		if( title !== contextTitle ) {
			this.node.cur.appendChild(
				el( 'a', 'xtoggle', m( 'tree-edit' ), function() { showSelection( title, obj ) } )
			);
		}
		return CustomExpander.prototype.root.apply( this, arguments );
	};
	TreeExpander.prototype.expandText = function( s ) {
		this.node.cur.appendChild( document.createTextNode( s ) );
		return s;
	};
	TreeExpander.prototype.template = function( obj ) {
		var old = this.node;
		this.node = obj.node = new TreeNode( obj, this.params && this.params.title );
		var result = CustomExpander.prototype.template.apply( this, arguments );
		var small = this.node.createResult( result );
		this.node = old;
		this.node.cur.appendChild( small );
		return result;
	};
	TreeExpander.prototype.expandTemplatePage = function( obj, page ) {
		this.node.type = 'template';
		this.node.body = this.node.cur;
		return CustomExpander.prototype.expandTemplatePage.apply( this, arguments );
	};
	TreeExpander.prototype.expandFunc = function( obj, name, arg ) {
		this.node.type = 'func';
		return CustomExpander.prototype.expandFunc.apply( this, arguments );
	};
	TreeExpander.prototype.tplarg = function( obj ) {
		var old = this.node;
		this.node = obj.node = new TreeNode( obj, this.params && this.params.title );
		this.node.type = 'tplarg';
		var result = CustomExpander.prototype.tplarg.apply( this, arguments );
		var small = this.node.createResult( result );
		this.node = old;
		this.node.cur.appendChild( small );
		return result;
	};
	TreeExpander.prototype.expandArg = function( obj, num, side ) {
		var old = this.node;
		this.node = obj.node;
		var oldcur = this.node.cur;
		this.node.cur = this.node.createPart( num, side );
		var result = CustomExpander.prototype.expandArg.apply( this, arguments );
		trimNode( this.node.cur );
		this.node.cur = oldcur;
		this.node = old;
		return result;
	};

	function trimNode( node ) {
		if( node.firstChild && node.firstChild.nodeType === 3 ) {
			node.firstChild.nodeValue = node.firstChild.nodeValue.replace( /^\s+/, '' );
		}
		if( node.lastChild && node.lastChild.nodeType === 3 ) {
			node.lastChild.nodeValue = node.lastChild.nodeValue.replace( /\s+$/, '' );
		}
		if( !node.innerHTML ) {
			node.appendChild( document.createTextNode( m( 'tree-empty' ) ) );
		}
	}
	function createTree() {
		expand( TreeExpander );
		$( '#tplutil-div-tree' ).empty().append( treeResult.node.cur );
	}
	function el( name, cls ) {
		var node = document.createElement( name );
		if( cls ) node.className = 'tplutil-' + cls;
		for( var i = 2; i < arguments.length; i++ ) {
			if( typeof arguments[i] === 'string' || typeof arguments[i] === 'number' ) {
				node.appendChild( document.createTextNode( arguments[i] ) );
			} else if( arguments[i].childNodes ) {
				node.appendChild( arguments[i] );
			} else if( typeof arguments[i] === 'function' ) {
				node.onclick = arguments[i];
			} else {
				for( var attr in arguments[i] ) if( arguments[i].hasOwnProperty( attr ) ) {
					node[attr] = arguments[i][attr];
				}
			}
		}
		if( name === 'a' && !node.href ) node.href = '#';
		return node;
	}
	function func( obj, func, args ) {
		return function() { obj[func].apply( obj, args || [] ); return false; };
	}
	var fakeDiv;
	function getTextHeight( el, text ) {
		// FIXME: This is an absurdly complicated hack to make Browsers (except IE) scroll
		// down to a selection. There must be a better way to do this
		var s = window.getComputedStyle( el, null );
		if( !fakeDiv ) {
			fakeDiv = document.createElement( 'div' );
			fakeDiv.style.fontSize = s.fontSize;
			fakeDiv.style.fontStyle = s.fontStyle;
			fakeDiv.style.fontWeight = s.fontWeight;
			fakeDiv.style.fontFamily = s.fontFamily;
			fakeDiv.style.lineHeight = s.lineHeight;
			fakeDiv.style.textTransform = s.textTransform;
			fakeDiv.style.letterSpacing = s.letterSpacing;
			fakeDiv.style.whiteSpace = 'pre-wrap';
			fakeDiv.style.position = 'absolute';
			fakeDiv.style.top = '-10000px';
			fakeDiv.style.left = '-10000px';
			fakeDiv.style.visibility = 'hidden';
			fakeDiv.appendChild( document.createTextNode( 'X' ) );
		}
		fakeDiv.style.width = s.width;
		fakeDiv.firstChild.nodeValue = text;
		document.body.appendChild( fakeDiv );
		var result = fakeDiv.clientHeight;
		document.body.removeChild( fakeDiv );
		return result;
	}

	/**********************************************************************************************
	 * Misc
	 */

	function CustomExpander() { mw.parser.TemplateExpander.apply( this, arguments ); }
	CustomExpander.prototype.getPageText = function( page ) {
		var title = this.cache.getTarget( page.ptitle );
		if( has.call( templates, title ) ) {
			return templates[title];
		} else {
			return mw.parser.TemplateExpander.prototype.getPageText.apply( this, arguments );
		}
	};
	CustomExpander.prototype.ext = function( obj ) {
		// We want to look into ref templates too
		if( obj.parts[1] && ( obj.extname === 'ref' || obj.extname === 'references' ) ) {
			var m = this.expandPart( obj.parts[0] ).match( /<([^\s\/>]*)([^>]*)>/ );
			var dom = this.preprocess( obj.parts[1][0] || '', false );
			var inner = this.expandPart( dom.parts[0] );
			var close = obj.parts[2] ? this.expandPart( obj.parts[2] ) : false;
			return this.expandExtension( m[1], m[2], inner, close );
		} else {
			return mw.parser.TemplateExpander.prototype.ext.apply( this, arguments );
		}
	};
	function expand( cls, cb ) {
		return mw.parser.iterate(
			cls,
			'expand',
			mw.parser.preprocess( templates[contextTitle] ),
			contextTitle,
			cb
		);
	}
	function loadTemplates( tab ) {
		$( '#tplutil-div-' + tab ).text( m( 'status-loaddeps' ) ).append( getSpinner() );
		var cb = tab === 'preview' ? createPreview : tab === 'tree' ? createTree : createReport;
		expand( CustomExpander, function() {
			$( '#tplutil-div-' + tab ).text( m( 'status-' + tab ) ).append( getSpinner() );
			setTimeout( cb, 0 );
		} );
	}
	function getSpinner() {
		return $( '<img>' ).attr( 'src', m( 'spinner-url' ) );
	}
	function m( key ) {
		arguments[0] = 'tplutil-' + arguments[0];
		return mw.msg.apply( mw.msg, arguments );
	}

})();

// </nowiki>