<?xml version="1.0"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
	<id>https://wiki.doikayt.org/index.php?action=history&amp;feed=atom&amp;title=MediaWiki%3ACommon.js</id>
	<title>MediaWiki:Common.js - Revision history</title>
	<link rel="self" type="application/atom+xml" href="https://wiki.doikayt.org/index.php?action=history&amp;feed=atom&amp;title=MediaWiki%3ACommon.js"/>
	<link rel="alternate" type="text/html" href="https://wiki.doikayt.org/index.php?title=MediaWiki:Common.js&amp;action=history"/>
	<updated>2026-09-12T11:27:58Z</updated>
	<subtitle>Revision history for this page on the wiki</subtitle>
	<generator>MediaWiki 1.46.0</generator>
	<entry>
		<id>https://wiki.doikayt.org/index.php?title=MediaWiki:Common.js&amp;diff=125&amp;oldid=prev</id>
		<title>Admin: qwiki: system/common-js.md</title>
		<link rel="alternate" type="text/html" href="https://wiki.doikayt.org/index.php?title=MediaWiki:Common.js&amp;diff=125&amp;oldid=prev"/>
		<updated>2026-08-12T02:35:26Z</updated>

		<summary type="html">&lt;p&gt;qwiki: system/common-js.md&lt;/p&gt;
&lt;p&gt;&lt;b&gt;New page&lt;/b&gt;&lt;/p&gt;&lt;div&gt;/* Point the site logo at the Doikayt Mobilization Labs homepage, opening&lt;br /&gt;
   in a new tab so the wiki stays open in the background. Locally, send it&lt;br /&gt;
   to the website preview server (npx serve website/dist -p 3456) instead&lt;br /&gt;
   of production, so clicking it doesn&amp;#039;t navigate away from localhost. */&lt;br /&gt;
( function () {&lt;br /&gt;
	&amp;#039;use strict&amp;#039;;&lt;br /&gt;
&lt;br /&gt;
	var isLocal = /^(localhost|127\.0\.0\.1)$/.test( location.hostname );&lt;br /&gt;
	var target  = isLocal ? &amp;#039;http://localhost:3456&amp;#039; : &amp;#039;https://www.doikayt.org&amp;#039;;&lt;br /&gt;
&lt;br /&gt;
	$( &amp;#039;#p-logo a&amp;#039; ).attr( {&lt;br /&gt;
		href: target,&lt;br /&gt;
		target: &amp;#039;_blank&amp;#039;,&lt;br /&gt;
		rel: &amp;#039;noopener&amp;#039;&lt;br /&gt;
	} );&lt;br /&gt;
}() );&lt;br /&gt;
&lt;br /&gt;
/* Same local/prod split for any content-authored link to doikayt.org (e.g. the&lt;br /&gt;
   About page&amp;#039;s links to the website) -- rewrite so they point at the local&lt;br /&gt;
   preview server instead of jumping out to production. */&lt;br /&gt;
( function () {&lt;br /&gt;
	&amp;#039;use strict&amp;#039;;&lt;br /&gt;
&lt;br /&gt;
	var isLocal = /^(localhost|127\.0\.0\.1)$/.test( location.hostname );&lt;br /&gt;
	if ( !isLocal ) { return; }&lt;br /&gt;
&lt;br /&gt;
	$( &amp;#039;#content a[href^=&amp;quot;https://www.doikayt.org/&amp;quot;]&amp;#039; ).each( function () {&lt;br /&gt;
		this.href = this.href.replace( &amp;#039;https://www.doikayt.org&amp;#039;, &amp;#039;http://localhost:3456&amp;#039; );&lt;br /&gt;
	} );&lt;br /&gt;
}() );&lt;br /&gt;
&lt;br /&gt;
/* Replace PageForms OOUI tooltip buttons with a plain &amp;lt;sup&amp;gt;?&amp;lt;/sup&amp;gt; trigger&lt;br /&gt;
   that shows the help text in a lightweight floating div on click. */&lt;br /&gt;
( function () {&lt;br /&gt;
	&amp;#039;use strict&amp;#039;;&lt;br /&gt;
&lt;br /&gt;
	mw.hook( &amp;#039;pf.formSetupAfter&amp;#039; ).add( function () {&lt;br /&gt;
		$( &amp;#039;th[data-tooltip]&amp;#039; ).each( function () {&lt;br /&gt;
			var $th  = $( this );&lt;br /&gt;
			var safe = $( &amp;#039;&amp;lt;div&amp;gt;&amp;#039; ).text( $th.attr( &amp;#039;data-tooltip&amp;#039; ) ).html();&lt;br /&gt;
&lt;br /&gt;
			var $trigger = $( &amp;#039;&amp;lt;sup class=&amp;quot;pf-tip&amp;quot;&amp;gt;?&amp;lt;/sup&amp;gt;&amp;#039; );&lt;br /&gt;
			var $popup   = $( &amp;#039;&amp;lt;div class=&amp;quot;pf-tip-popup&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;&amp;#039; )&lt;br /&gt;
			                   .html( &amp;#039;&amp;lt;p&amp;gt;&amp;#039; + safe + &amp;#039;&amp;lt;/p&amp;gt;&amp;#039; )&lt;br /&gt;
			                   .hide();&lt;br /&gt;
&lt;br /&gt;
			$trigger.on( &amp;#039;click&amp;#039;, function ( e ) {&lt;br /&gt;
				e.stopPropagation();&lt;br /&gt;
				$( &amp;#039;.pf-tip-popup&amp;#039; ).not( $popup ).hide();&lt;br /&gt;
				$popup.toggle();&lt;br /&gt;
			} );&lt;br /&gt;
&lt;br /&gt;
			$th.css( &amp;#039;position&amp;#039;, &amp;#039;relative&amp;#039; ).append( $trigger ).append( $popup );&lt;br /&gt;
		} );&lt;br /&gt;
&lt;br /&gt;
		$( document ).on( &amp;#039;click.pf-tip&amp;#039;, function () {&lt;br /&gt;
			$( &amp;#039;.pf-tip-popup&amp;#039; ).hide();&lt;br /&gt;
		} );&lt;br /&gt;
	} );&lt;br /&gt;
}() );&lt;br /&gt;
&lt;br /&gt;
/* Probe the URL field on the Tool form for reachability on blur.&lt;br /&gt;
   Uses no-cors so any server response (any status code) counts as reachable;&lt;br /&gt;
   only genuine network failure (DNS, TLS, timeout) is flagged as unreachable. */&lt;br /&gt;
( function () {&lt;br /&gt;
	&amp;#039;use strict&amp;#039;;&lt;br /&gt;
&lt;br /&gt;
	// null = not yet checked, true = reachable, false = unreachable&lt;br /&gt;
	var probeResult = null;&lt;br /&gt;
	var probedUrl   = &amp;#039;&amp;#039;;&lt;br /&gt;
&lt;br /&gt;
	function getFullUrl() {&lt;br /&gt;
		var $protocol = $( &amp;#039;select[name=&amp;quot;Tool[protocol]&amp;quot;]&amp;#039; );&lt;br /&gt;
		var protocol  = $protocol.length ? $protocol.val().trim() : &amp;#039;https&amp;#039;;&lt;br /&gt;
		var domain    = $( &amp;#039;input[name=&amp;quot;Tool[url]&amp;quot;]&amp;#039; ).val().trim();&lt;br /&gt;
		return domain ? protocol + &amp;#039;://&amp;#039; + domain : &amp;#039;&amp;#039;;&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function showStatus( msg, color ) {&lt;br /&gt;
		$( &amp;#039;#pf-url-status&amp;#039; ).text( msg ).css( &amp;#039;color&amp;#039;, color );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function probe( url ) {&lt;br /&gt;
		if ( !url || url === probedUrl ) { return; }&lt;br /&gt;
		probedUrl   = url;&lt;br /&gt;
		probeResult = null;&lt;br /&gt;
		showStatus( &amp;#039;Checking…&amp;#039;, &amp;#039;gray&amp;#039; );&lt;br /&gt;
&lt;br /&gt;
		var timeout = new Promise( function ( _, reject ) {&lt;br /&gt;
			setTimeout( function () { reject( new Error( &amp;#039;timeout&amp;#039; ) ); }, 8000 );&lt;br /&gt;
		} );&lt;br /&gt;
&lt;br /&gt;
		Promise.race( [&lt;br /&gt;
			fetch( url, { method: &amp;#039;HEAD&amp;#039;, mode: &amp;#039;no-cors&amp;#039;, cache: &amp;#039;no-cache&amp;#039; } ),&lt;br /&gt;
			timeout&lt;br /&gt;
		] ).then( function () {&lt;br /&gt;
			if ( url !== probedUrl ) { return; }&lt;br /&gt;
			probeResult = true;&lt;br /&gt;
			showStatus( &amp;#039;✓ Reachable&amp;#039;, &amp;#039;green&amp;#039; );&lt;br /&gt;
		} ).catch( function () {&lt;br /&gt;
			if ( url !== probedUrl ) { return; }&lt;br /&gt;
			probeResult = false;&lt;br /&gt;
			showStatus( &amp;#039;✗ Not reachable&amp;#039;, &amp;#039;red&amp;#039; );&lt;br /&gt;
		} );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	$( function () {&lt;br /&gt;
		var $url = $( &amp;#039;input[name=&amp;quot;Tool[url]&amp;quot;]&amp;#039; );&lt;br /&gt;
		if ( !$url.length ) { return; }&lt;br /&gt;
&lt;br /&gt;
		$url.closest( &amp;#039;td&amp;#039; ).append(&lt;br /&gt;
			$( &amp;#039;&amp;lt;span&amp;gt;&amp;#039; ).attr( &amp;#039;id&amp;#039;, &amp;#039;pf-url-status&amp;#039; ).css( { marginLeft: &amp;#039;8px&amp;#039;, fontSize: &amp;#039;0.9em&amp;#039; } )&lt;br /&gt;
		);&lt;br /&gt;
&lt;br /&gt;
		$url.on( &amp;#039;blur&amp;#039;, function () {&lt;br /&gt;
			probe( getFullUrl() );&lt;br /&gt;
		} );&lt;br /&gt;
	} );&lt;br /&gt;
&lt;br /&gt;
	// Clear previous error rows before each validation run&lt;br /&gt;
	mw.hook( &amp;#039;pf.formValidation&amp;#039; ).add( function () {&lt;br /&gt;
		$( &amp;#039;.pf-error-row&amp;#039; ).remove();&lt;br /&gt;
	} );&lt;br /&gt;
&lt;br /&gt;
	mw.hook( &amp;#039;pf.formValidation&amp;#039; ).add( function ( args ) {&lt;br /&gt;
		var $url = $( &amp;#039;input[name=&amp;quot;Tool[url]&amp;quot;]&amp;#039; );&lt;br /&gt;
		if ( !$url.length ) { return; }&lt;br /&gt;
&lt;br /&gt;
		var domain = $url.val().trim();&lt;br /&gt;
		var msg = null;&lt;br /&gt;
&lt;br /&gt;
		if ( !domain ) {&lt;br /&gt;
			msg = &amp;#039;URL is required.&amp;#039;;&lt;br /&gt;
		} else {&lt;br /&gt;
			var url = getFullUrl();&lt;br /&gt;
			if ( probeResult === false ) {&lt;br /&gt;
				msg = &amp;#039;URL is not reachable. Please verify the address.&amp;#039;;&lt;br /&gt;
			} else if ( probeResult === null ) {&lt;br /&gt;
				showStatus( &amp;#039;Checking…&amp;#039;, &amp;#039;gray&amp;#039; );&lt;br /&gt;
				probe( url );&lt;br /&gt;
				msg = &amp;#039;Verifying URL reachability — please try saving again in a moment.&amp;#039;;&lt;br /&gt;
			}&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		if ( msg ) {&lt;br /&gt;
			args.numErrors++;&lt;br /&gt;
			$( &amp;#039;&amp;lt;tr class=&amp;quot;pf-error-row&amp;quot;&amp;gt;&amp;lt;td colspan=&amp;quot;2&amp;quot;&amp;gt;&amp;lt;/td&amp;gt;&amp;lt;/tr&amp;gt;&amp;#039; )&lt;br /&gt;
				.find( &amp;#039;td&amp;#039; ).text( msg ).end()&lt;br /&gt;
				.insertAfter( $url.closest( &amp;#039;tr&amp;#039; ) );&lt;br /&gt;
			setTimeout( function () {&lt;br /&gt;
				var $e = $( &amp;#039;.pf-error-row&amp;#039; ).first();&lt;br /&gt;
				if ( $e.length ) { $e[ 0 ].scrollIntoView( { behavior: &amp;#039;smooth&amp;#039;, block: &amp;#039;nearest&amp;#039; } ); }&lt;br /&gt;
			}, 0 );&lt;br /&gt;
		}&lt;br /&gt;
		// probeResult === true: allow through&lt;br /&gt;
	} );&lt;br /&gt;
}() );&lt;br /&gt;
&lt;br /&gt;
/* Require at least one category before the Tool form can be submitted. */&lt;br /&gt;
( function () {&lt;br /&gt;
	&amp;#039;use strict&amp;#039;;&lt;br /&gt;
&lt;br /&gt;
	mw.hook( &amp;#039;pf.formValidation&amp;#039; ).add( function ( args ) {&lt;br /&gt;
		var $category = $( &amp;#039;[name=&amp;quot;Tool[category]&amp;quot;]&amp;#039; );&lt;br /&gt;
		if ( !$category.length ) { return; }&lt;br /&gt;
		if ( !( $category.val() || &amp;#039;&amp;#039; ).trim() ) {&lt;br /&gt;
			args.numErrors++;&lt;br /&gt;
			$( &amp;#039;&amp;lt;tr class=&amp;quot;pf-error-row&amp;quot;&amp;gt;&amp;lt;td colspan=&amp;quot;2&amp;quot;&amp;gt;Please select a category.&amp;lt;/td&amp;gt;&amp;lt;/tr&amp;gt;&amp;#039; )&lt;br /&gt;
				.insertAfter( $category.closest( &amp;#039;tr&amp;#039; ) );&lt;br /&gt;
			setTimeout( function () {&lt;br /&gt;
				var $e = $( &amp;#039;.pf-error-row&amp;#039; ).first();&lt;br /&gt;
				if ( $e.length ) { $e[ 0 ].scrollIntoView( { behavior: &amp;#039;smooth&amp;#039;, block: &amp;#039;nearest&amp;#039; } ); }&lt;br /&gt;
			}, 0 );&lt;br /&gt;
		}&lt;br /&gt;
	} );&lt;br /&gt;
}() );&lt;br /&gt;
&lt;br /&gt;
/* Amount / pricing cross-validation. Two rules:&lt;br /&gt;
   1. pricing=free + non-zero amount → error (amount must be blank for free tools)&lt;br /&gt;
   2. pricing!=free + blank or zero amount → error (amount is required) */&lt;br /&gt;
( function () {&lt;br /&gt;
	&amp;#039;use strict&amp;#039;;&lt;br /&gt;
&lt;br /&gt;
	mw.hook( &amp;#039;pf.formValidation&amp;#039; ).add( function ( args ) {&lt;br /&gt;
		var $pricing = $( &amp;#039;[name=&amp;quot;Tool[pricing]&amp;quot;]&amp;#039; );&lt;br /&gt;
		var $amount  = $( &amp;#039;[name=&amp;quot;Tool[amount]&amp;quot;]&amp;#039; );&lt;br /&gt;
		if ( !$pricing.length || !$amount.length ) { return; }&lt;br /&gt;
&lt;br /&gt;
		var pricing = $pricing.val().trim();&lt;br /&gt;
		var amount  = ( $amount.val() || &amp;#039;&amp;#039; ).trim();&lt;br /&gt;
		var numericAmount = parseFloat( amount );&lt;br /&gt;
&lt;br /&gt;
		if ( pricing === &amp;#039;free&amp;#039; &amp;amp;&amp;amp; amount &amp;amp;&amp;amp; numericAmount &amp;gt; 0 ) {&lt;br /&gt;
			args.numErrors++;&lt;br /&gt;
			$( &amp;#039;&amp;lt;tr class=&amp;quot;pf-error-row&amp;quot;&amp;gt;&amp;lt;td colspan=&amp;quot;2&amp;quot;&amp;gt;Free tools must have a blank amount.&amp;lt;/td&amp;gt;&amp;lt;/tr&amp;gt;&amp;#039; )&lt;br /&gt;
				.insertAfter( $amount.closest( &amp;#039;tr&amp;#039; ) );&lt;br /&gt;
			setTimeout( function () {&lt;br /&gt;
				var $e = $( &amp;#039;.pf-error-row&amp;#039; ).first();&lt;br /&gt;
				if ( $e.length ) { $e[ 0 ].scrollIntoView( { behavior: &amp;#039;smooth&amp;#039;, block: &amp;#039;nearest&amp;#039; } ); }&lt;br /&gt;
			}, 0 );&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		if ( pricing !== &amp;#039;free&amp;#039; &amp;amp;&amp;amp; ( !amount || numericAmount === 0 ) ) {&lt;br /&gt;
			args.numErrors++;&lt;br /&gt;
			$( &amp;#039;&amp;lt;tr class=&amp;quot;pf-error-row&amp;quot;&amp;gt;&amp;lt;td colspan=&amp;quot;2&amp;quot;&amp;gt;Amount is required when pricing is not free.&amp;lt;/td&amp;gt;&amp;lt;/tr&amp;gt;&amp;#039; )&lt;br /&gt;
				.insertAfter( $amount.closest( &amp;#039;tr&amp;#039; ) );&lt;br /&gt;
			setTimeout( function () {&lt;br /&gt;
				var $e = $( &amp;#039;.pf-error-row&amp;#039; ).first();&lt;br /&gt;
				if ( $e.length ) { $e[ 0 ].scrollIntoView( { behavior: &amp;#039;smooth&amp;#039;, block: &amp;#039;nearest&amp;#039; } ); }&lt;br /&gt;
			}, 0 );&lt;br /&gt;
		}&lt;br /&gt;
	} );&lt;br /&gt;
}() );&lt;br /&gt;
&lt;br /&gt;
/* Sidebar CategoryTree:&lt;br /&gt;
   - Leaf (page) labels: strip &amp;quot; - tagline&amp;quot; suffix so only the product name&lt;br /&gt;
     shows, and move the full &amp;quot;Name - tagline&amp;quot; text onto the title attribute&lt;br /&gt;
     so it&amp;#039;s still available as a hover tooltip. Category listing pages are&lt;br /&gt;
     unaffected -- only the sidebar tree text is touched.&lt;br /&gt;
   - Category labels: replace the native (redundant, page-name-only) title&lt;br /&gt;
     tooltip with the category&amp;#039;s one-line description, pulled from the first&lt;br /&gt;
     paragraph of the category page&amp;#039;s wikitext via a batched API call.&lt;br /&gt;
   MutationObserver covers nodes loaded via AJAX on expansion. */&lt;br /&gt;
( function () {&lt;br /&gt;
	&amp;#039;use strict&amp;#039;;&lt;br /&gt;
&lt;br /&gt;
	var categoryDescCache   = {};&lt;br /&gt;
	var categoryDescPending = {};&lt;br /&gt;
&lt;br /&gt;
	function firstParagraph( wikitext ) {&lt;br /&gt;
		var lines = ( wikitext || &amp;#039;&amp;#039; ).split( &amp;#039;\n&amp;#039; );&lt;br /&gt;
		for ( var i = 0; i &amp;lt; lines.length; i++ ) {&lt;br /&gt;
			// Strip raw HTML first -- pandoc&amp;#039;s markdown-&amp;gt;mediawiki writer emits a&lt;br /&gt;
			// &amp;lt;span id=&amp;quot;...&amp;quot;&amp;gt;&amp;lt;/span&amp;gt; anchor immediately before every heading, which&lt;br /&gt;
			// would otherwise be picked up as the &amp;quot;first line&amp;quot; of body text.&lt;br /&gt;
			var line = lines[ i ].trim().replace( /&amp;lt;[^&amp;gt;]+&amp;gt;/g, &amp;#039;&amp;#039; ).trim();&lt;br /&gt;
			if ( !line || /^=+/.test( line ) ) { continue; }&lt;br /&gt;
			return line.replace( /\[\[(?:[^\]|]+\|)?([^\]]+)\]\]/g, &amp;#039;$1&amp;#039; );&lt;br /&gt;
		}&lt;br /&gt;
		return &amp;#039;&amp;#039;;&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function applyCategoryDescriptions( titleToDesc ) {&lt;br /&gt;
		$( &amp;#039;#mw-panel .CategoryTreeItem a&amp;#039; ).each( function () {&lt;br /&gt;
			var $a  = $( this );&lt;br /&gt;
			var cur = $a.attr( &amp;#039;title&amp;#039; );&lt;br /&gt;
			if ( cur &amp;amp;&amp;amp; titleToDesc[ cur ] ) { $a.attr( &amp;#039;title&amp;#039;, titleToDesc[ cur ] ); }&lt;br /&gt;
		} );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function fetchCategoryDescriptions( titles ) {&lt;br /&gt;
		var batches = [];&lt;br /&gt;
		for ( var i = 0; i &amp;lt; titles.length; i += 50 ) { batches.push( titles.slice( i, i + 50 ) ); }&lt;br /&gt;
&lt;br /&gt;
		batches.forEach( function ( batch ) {&lt;br /&gt;
			new mw.Api().get( {&lt;br /&gt;
				action: &amp;#039;query&amp;#039;,&lt;br /&gt;
				titles: batch.join( &amp;#039;|&amp;#039; ),&lt;br /&gt;
				prop: &amp;#039;revisions&amp;#039;,&lt;br /&gt;
				rvprop: &amp;#039;content&amp;#039;,&lt;br /&gt;
				rvslots: &amp;#039;main&amp;#039;,&lt;br /&gt;
				formatversion: 2&lt;br /&gt;
			} ).then( function ( data ) {&lt;br /&gt;
				var titleToDesc = {};&lt;br /&gt;
				( ( data.query &amp;amp;&amp;amp; data.query.pages ) || [] ).forEach( function ( page ) {&lt;br /&gt;
					batch.forEach( function ( t ) { delete categoryDescPending[ t ]; } );&lt;br /&gt;
					if ( !page.revisions ) { return; }&lt;br /&gt;
					var desc = firstParagraph( page.revisions[ 0 ].slots.main.content );&lt;br /&gt;
					if ( !desc ) { return; }&lt;br /&gt;
					categoryDescCache[ page.title ] = desc;&lt;br /&gt;
					titleToDesc[ page.title ] = desc;&lt;br /&gt;
				} );&lt;br /&gt;
				applyCategoryDescriptions( titleToDesc );&lt;br /&gt;
			} );&lt;br /&gt;
		} );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function normalizeTree( root ) {&lt;br /&gt;
		var toFetch = [];&lt;br /&gt;
&lt;br /&gt;
		$( root ).find( &amp;#039;#mw-panel .CategoryTreeItem a&amp;#039; ).each( function () {&lt;br /&gt;
			var $a    = $( this );&lt;br /&gt;
			var href  = $a.attr( &amp;#039;href&amp;#039; ) || &amp;#039;&amp;#039;;&lt;br /&gt;
			var label = $a.attr( &amp;#039;title&amp;#039; ) || &amp;#039;&amp;#039;;&lt;br /&gt;
			var isCategory = /\/Category:/.test( href ) || /^Category:/.test( label );&lt;br /&gt;
&lt;br /&gt;
			if ( isCategory ) {&lt;br /&gt;
				if ( !label ) { return; }&lt;br /&gt;
				if ( categoryDescCache[ label ] ) {&lt;br /&gt;
					$a.attr( &amp;#039;title&amp;#039;, categoryDescCache[ label ] );&lt;br /&gt;
				} else if ( !categoryDescPending[ label ] ) {&lt;br /&gt;
					categoryDescPending[ label ] = true;&lt;br /&gt;
					toFetch.push( label );&lt;br /&gt;
				}&lt;br /&gt;
				return;&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			var t = $a.text(), i = t.indexOf( &amp;#039; - &amp;#039; );&lt;br /&gt;
			if ( i !== -1 ) {&lt;br /&gt;
				$a.attr( &amp;#039;title&amp;#039;, t );&lt;br /&gt;
				$a.text( t.slice( 0, i ) );&lt;br /&gt;
			}&lt;br /&gt;
		} );&lt;br /&gt;
&lt;br /&gt;
		if ( toFetch.length ) { fetchCategoryDescriptions( toFetch ); }&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	$( function () {&lt;br /&gt;
		var panel = document.getElementById( &amp;#039;mw-panel&amp;#039; );&lt;br /&gt;
		if ( !panel ) { return; }&lt;br /&gt;
		normalizeTree( document );&lt;br /&gt;
		new MutationObserver( function ( mutations ) {&lt;br /&gt;
			mutations.forEach( function ( m ) {&lt;br /&gt;
				m.addedNodes.forEach( function ( n ) {&lt;br /&gt;
					if ( n.nodeType === 1 ) { normalizeTree( n ); }&lt;br /&gt;
				} );&lt;br /&gt;
			} );&lt;br /&gt;
		} ).observe( panel, { childList: true, subtree: true } );&lt;br /&gt;
	} );&lt;br /&gt;
}() );&lt;br /&gt;
&lt;br /&gt;
/* Inject a &amp;quot;built at &amp;lt;commit&amp;gt;&amp;quot; link into the wiki footer. The commit hash is&lt;br /&gt;
   stored in MediaWiki:Doikayt-build-commit and written on every qwiki deploy. */&lt;br /&gt;
( function () {&lt;br /&gt;
	&amp;#039;use strict&amp;#039;;&lt;br /&gt;
&lt;br /&gt;
	var GITHUB_REPO = &amp;#039;https://github.com/doikayt/qwiki&amp;#039;;&lt;br /&gt;
&lt;br /&gt;
	new mw.Api().get( {&lt;br /&gt;
		action: &amp;#039;query&amp;#039;,&lt;br /&gt;
		titles: &amp;#039;MediaWiki:Doikayt-build-commit&amp;#039;,&lt;br /&gt;
		prop: &amp;#039;revisions&amp;#039;,&lt;br /&gt;
		rvprop: &amp;#039;content&amp;#039;,&lt;br /&gt;
		rvslots: &amp;#039;main&amp;#039;&lt;br /&gt;
	} ).then( function ( data ) {&lt;br /&gt;
		var pages = data.query.pages;&lt;br /&gt;
		var page  = pages[ Object.keys( pages )[ 0 ] ];&lt;br /&gt;
		if ( !page.revisions ) { return; }&lt;br /&gt;
		var commit = page.revisions[ 0 ].slots.main[ &amp;#039;*&amp;#039; ].trim();&lt;br /&gt;
		if ( !commit ) { return; }&lt;br /&gt;
		var short = commit.slice( 0, 7 );&lt;br /&gt;
		var $link = $( &amp;#039;&amp;lt;a&amp;gt;&amp;#039; ).attr( {&lt;br /&gt;
			href:   GITHUB_REPO + &amp;#039;/commit/&amp;#039; + commit,&lt;br /&gt;
			target: &amp;#039;_blank&amp;#039;,&lt;br /&gt;
			rel:    &amp;#039;noopener&amp;#039;&lt;br /&gt;
		} ).text( &amp;#039;built at &amp;#039; + short );&lt;br /&gt;
		$( &amp;#039;#footer-info&amp;#039; ).append(&lt;br /&gt;
			$( &amp;#039;&amp;lt;li&amp;gt;&amp;#039; ).attr( &amp;#039;id&amp;#039;, &amp;#039;footer-info-buildcommit&amp;#039; ).append( $link )&lt;br /&gt;
		);&lt;br /&gt;
	} );&lt;br /&gt;
}() );&lt;/div&gt;</summary>
		<author><name>Admin</name></author>
	</entry>
</feed>