<?xml version="1.0"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
	<id>https://deadlock.wiki/MediaWiki:Gadget-blame.js?action=history&amp;feed=atom</id>
	<title>MediaWiki:Gadget-blame.js - Revision history</title>
	<link rel="self" type="application/atom+xml" href="https://deadlock.wiki/MediaWiki:Gadget-blame.js?action=history&amp;feed=atom"/>
	<link rel="alternate" type="text/html" href="https://deadlock.wiki/MediaWiki:Gadget-blame.js?action=history"/>
	<updated>2026-09-11T06:01:41Z</updated>
	<subtitle>Revision history for this page on the wiki</subtitle>
	<generator>MediaWiki 1.46.0</generator>
	<entry>
		<id>https://deadlock.wiki/MediaWiki:Gadget-blame.js?diff=126547&amp;oldid=prev</id>
		<title>Kai: Created page with &quot;/*  * Gadget: Blame — git blame for articles. Colours every word by the editor who  * last changed it; words a template or module produced are hatched instead.  * --&gt; https://starcitizen.tools/index.php?title=MediaWiki:Gadget-blame.js&amp;action=edit &lt;--  * Special:Export/&lt;page&gt;?history=1 is the transport because the Action API&#039;s  * 50-row content cap does not apply there and $wgExportMaxHistory is not  * consulted for GET. It also costs the server about a third of what th...&quot;</title>
		<link rel="alternate" type="text/html" href="https://deadlock.wiki/MediaWiki:Gadget-blame.js?diff=126547&amp;oldid=prev"/>
		<updated>2026-09-05T10:05:44Z</updated>

		<summary type="html">&lt;p&gt;Created page with &amp;quot;/*  * Gadget: Blame — git blame for articles. Colours every word by the editor who  * last changed it; words a template or module produced are hatched instead.  * --&amp;gt; https://starcitizen.tools/index.php?title=MediaWiki:Gadget-blame.js&amp;amp;action=edit &amp;lt;--  * Special:Export/&amp;lt;page&amp;gt;?history=1 is the transport because the Action API&amp;#039;s  * 50-row content cap does not apply there and $wgExportMaxHistory is not  * consulted for GET. It also costs the server about a third of what th...&amp;quot;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;New page&lt;/b&gt;&lt;/p&gt;&lt;div&gt;/*&lt;br /&gt;
 * Gadget: Blame — git blame for articles. Colours every word by the editor who&lt;br /&gt;
 * last changed it; words a template or module produced are hatched instead.&lt;br /&gt;
 * --&amp;gt; https://starcitizen.tools/index.php?title=MediaWiki:Gadget-blame.js&amp;amp;action=edit &amp;lt;--&lt;br /&gt;
 * Special:Export/&amp;lt;page&amp;gt;?history=1 is the transport because the Action API&amp;#039;s&lt;br /&gt;
 * 50-row content cap does not apply there and $wgExportMaxHistory is not&lt;br /&gt;
 * consulted for GET. It also costs the server about a third of what the obvious&lt;br /&gt;
 * alternative does: paging content by revision id spends more total server time&lt;br /&gt;
 * and spreads it over ten requests instead of one. A wiki with&lt;br /&gt;
 * $wgExportAllowHistory=false returns ONE revision rather than an error, hence&lt;br /&gt;
 * the fallback to prop=revisions.&lt;br /&gt;
 *&lt;br /&gt;
 * Parsoid is never consulted. Its source offsets cannot attribute template&lt;br /&gt;
 * parameter prose or the reference list, and its HTML can carry unexpanded&lt;br /&gt;
 * {{#parsoidfragment}} markers (T432547).&lt;br /&gt;
 *&lt;br /&gt;
 * Codex components are avoided for bundle size: the chrome is this gadget&amp;#039;s own&lt;br /&gt;
 * CSS over Codex design tokens, and the popover is the platform&amp;#039;s own.&lt;br /&gt;
 */&lt;br /&gt;
( function () {&lt;br /&gt;
	&amp;#039;use strict&amp;#039;;&lt;br /&gt;
&lt;br /&gt;
	const text = require( &amp;#039;./blame-text.js&amp;#039; );&lt;br /&gt;
	const teleportTarget = require( &amp;#039;mediawiki.page.ready&amp;#039; ).teleportTarget;&lt;br /&gt;
&lt;br /&gt;
	// Time-based, not every N revisions: per-revision cost varies by orders of&lt;br /&gt;
	// magnitude, so a fixed count still leaves 50 ms+ tasks.&lt;br /&gt;
	const SLICE_MS = 16;&lt;br /&gt;
	const PALETTE_SIZE = 8;&lt;br /&gt;
	const HIDDEN = &amp;#039;(hidden)&amp;#039;;&lt;br /&gt;
	// Past the cap the unmatched middle is credited to the newer revision, so a&lt;br /&gt;
	// cap below a plausible scattered sweep hands one editor text they never&lt;br /&gt;
	// wrote. Myers costs O(cap^2) once exhausted.&lt;br /&gt;
	const MAX_D_REVISION = 5000;&lt;br /&gt;
	// Page-to-source diverges much further, since template output has no source&lt;br /&gt;
	// counterpart at all: 3565 measured on an infobox fed from an API. Exhausting&lt;br /&gt;
	// this hatches the WHOLE page rather than the excess.&lt;br /&gt;
	const MAX_D_ALIGN = 20000;&lt;br /&gt;
	// &amp;lt;style&amp;gt; is here because TemplateStyles injects it inside .mw-parser-output.&lt;br /&gt;
	// The reference LIST is deliberately absent: moveRefs makes it attributable.&lt;br /&gt;
	//&lt;br /&gt;
	// Template classes must NOT be added. Their parameter values stay in the&lt;br /&gt;
	// source stream, and the alignment is a monotone LCS, so excluding the&lt;br /&gt;
	// rendered region strands those tokens for the first body word that matches.&lt;br /&gt;
	const UNPAINTED = &amp;#039;.mw-editsection, sup.reference, .mw-cite-backlink, style, script, .gadget-blame-popover, .gadget-blame-legend&amp;#039;;&lt;br /&gt;
	// cdxIconClose, inlined: @wikimedia/codex-icons is not a ResourceLoader module.&lt;br /&gt;
	const ICON_CLOSE = &amp;#039;M16.707 4.707 11.414 10l5.293 5.293-1.414 1.414L10 11.414l-5.293 5.293-1.414-1.414L8.586 10 3.293 4.707l1.414-1.414L10 8.586l5.293-5.293z&amp;#039;;&lt;br /&gt;
&lt;br /&gt;
	const config = mw.config.get( [&lt;br /&gt;
		&amp;#039;wgIsArticle&amp;#039;, &amp;#039;wgArticleId&amp;#039;, &amp;#039;wgAction&amp;#039;, &amp;#039;wgPageName&amp;#039;, &amp;#039;wgRevisionId&amp;#039;,&lt;br /&gt;
		&amp;#039;wgCurRevisionId&amp;#039;&lt;br /&gt;
	] );&lt;br /&gt;
	if ( !config.wgIsArticle || !config.wgArticleId || config.wgAction !== &amp;#039;view&amp;#039; ) {&lt;br /&gt;
		return;&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	let historyPromise = null;&lt;br /&gt;
	const page = { on: false, token: 0, painted: [], legend: null, revById: null };&lt;br /&gt;
&lt;br /&gt;
	function el( tag, attrs, children ) {&lt;br /&gt;
		const node = document.createElement( tag );&lt;br /&gt;
		Object.keys( attrs || {} ).forEach( ( k ) =&amp;gt; {&lt;br /&gt;
			if ( k === &amp;#039;class&amp;#039; ) {&lt;br /&gt;
				node.className = attrs[ k ];&lt;br /&gt;
			} else if ( k === &amp;#039;text&amp;#039; ) {&lt;br /&gt;
				node.textContent = attrs[ k ];&lt;br /&gt;
			} else {&lt;br /&gt;
				node.setAttribute( k, attrs[ k ] );&lt;br /&gt;
			}&lt;br /&gt;
		} );&lt;br /&gt;
		( children || [] ).forEach( ( c ) =&amp;gt; {&lt;br /&gt;
			if ( c ) {&lt;br /&gt;
				node.appendChild( typeof c === &amp;#039;string&amp;#039; ? document.createTextNode( c ) : c );&lt;br /&gt;
			}&lt;br /&gt;
		} );&lt;br /&gt;
		return node;&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function link( href, label, cls ) {&lt;br /&gt;
		return el( &amp;#039;a&amp;#039;, { href, class: cls || &amp;#039;&amp;#039;, text: label } );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function iconButton( label, pathD ) {&lt;br /&gt;
		const svgNs = &amp;#039;http://www.w3.org/2000/svg&amp;#039;;&lt;br /&gt;
		const svg = document.createElementNS( svgNs, &amp;#039;svg&amp;#039; );&lt;br /&gt;
		svg.setAttribute( &amp;#039;width&amp;#039;, &amp;#039;20&amp;#039; );&lt;br /&gt;
		svg.setAttribute( &amp;#039;height&amp;#039;, &amp;#039;20&amp;#039; );&lt;br /&gt;
		svg.setAttribute( &amp;#039;viewBox&amp;#039;, &amp;#039;0 0 20 20&amp;#039; );&lt;br /&gt;
		svg.setAttribute( &amp;#039;aria-hidden&amp;#039;, &amp;#039;true&amp;#039; );&lt;br /&gt;
		const path = document.createElementNS( svgNs, &amp;#039;path&amp;#039; );&lt;br /&gt;
		path.setAttribute( &amp;#039;d&amp;#039;, pathD );&lt;br /&gt;
		svg.appendChild( path );&lt;br /&gt;
		return el( &amp;#039;button&amp;#039;, {&lt;br /&gt;
			type: &amp;#039;button&amp;#039;,&lt;br /&gt;
			class: &amp;#039;gadget-blame__button&amp;#039;,&lt;br /&gt;
			&amp;#039;aria-label&amp;#039;: label,&lt;br /&gt;
			title: label&lt;br /&gt;
		}, [ svg ] );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function message( type, children ) {&lt;br /&gt;
		return el( &amp;#039;div&amp;#039;, {&lt;br /&gt;
			class: &amp;#039;gadget-blame__notice gadget-blame__notice--&amp;#039; + type,&lt;br /&gt;
			&amp;#039;aria-live&amp;#039;: &amp;#039;polite&amp;#039;&lt;br /&gt;
		}, children );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function formatDate( iso ) {&lt;br /&gt;
		const d = new Date( iso );&lt;br /&gt;
		return isNaN( d ) ? iso : d.toLocaleString( undefined, { dateStyle: &amp;#039;medium&amp;#039;, timeStyle: &amp;#039;short&amp;#039; } );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function userLink( name ) {&lt;br /&gt;
		if ( !name ) {&lt;br /&gt;
			return el( &amp;#039;span&amp;#039;, { class: &amp;#039;gadget-blame__meta&amp;#039;, text: HIDDEN } );&lt;br /&gt;
		}&lt;br /&gt;
		const isIp = mw.util.isIPAddress ? mw.util.isIPAddress( name ) : /^\d+\.\d+\.\d+\.\d+$/.test( name );&lt;br /&gt;
		const target = isIp ? &amp;#039;Special:Contributions/&amp;#039; + name : &amp;#039;User:&amp;#039; + name;&lt;br /&gt;
		return link( mw.util.getUrl( target ), name, &amp;#039;gadget-blame__user&amp;#039; );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function contentRoot() {&lt;br /&gt;
		return document.querySelector( &amp;#039;#mw-content-text .mw-parser-output&amp;#039; );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function tick() {&lt;br /&gt;
		return new Promise( ( resolve ) =&amp;gt; setTimeout( resolve ) );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function textOf( parent, selector ) {&lt;br /&gt;
		const n = parent.querySelector( selector );&lt;br /&gt;
		return n ? n.textContent : &amp;#039;&amp;#039;;&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function parseExport( xml ) {&lt;br /&gt;
		const doc = new DOMParser().parseFromString( xml, &amp;#039;application/xml&amp;#039; );&lt;br /&gt;
		if ( doc.querySelector( &amp;#039;parsererror&amp;#039; ) ) {&lt;br /&gt;
			throw new Error( &amp;#039;blame-export-parse&amp;#039; );&lt;br /&gt;
		}&lt;br /&gt;
		const revs = [];&lt;br /&gt;
		doc.querySelectorAll( &amp;#039;revision&amp;#039; ).forEach( ( r ) =&amp;gt; {&lt;br /&gt;
			const t = r.querySelector( &amp;#039;:scope &amp;gt; text&amp;#039; );&lt;br /&gt;
			const hidden = !t || t.getAttribute( &amp;#039;deleted&amp;#039; ) === &amp;#039;deleted&amp;#039;;&lt;br /&gt;
			revs.push( {&lt;br /&gt;
				id: Number( textOf( r, &amp;#039;:scope &amp;gt; id&amp;#039; ) ),&lt;br /&gt;
				parentid: Number( textOf( r, &amp;#039;:scope &amp;gt; parentid&amp;#039; ) || 0 ),&lt;br /&gt;
				timestamp: textOf( r, &amp;#039;:scope &amp;gt; timestamp&amp;#039; ),&lt;br /&gt;
				user: textOf( r, &amp;#039;:scope &amp;gt; contributor &amp;gt; username&amp;#039; ) || textOf( r, &amp;#039;:scope &amp;gt; contributor &amp;gt; ip&amp;#039; ),&lt;br /&gt;
				comment: textOf( r, &amp;#039;:scope &amp;gt; comment&amp;#039; ),&lt;br /&gt;
				sha1: textOf( r, &amp;#039;:scope &amp;gt; sha1&amp;#039; ) || ( t &amp;amp;&amp;amp; t.getAttribute( &amp;#039;sha1&amp;#039; ) ) || &amp;#039;&amp;#039;,&lt;br /&gt;
				// null, not &amp;#039;&amp;#039;: revision-deleted text is excluded, not an empty page.&lt;br /&gt;
				text: hidden ? null : t.textContent&lt;br /&gt;
			} );&lt;br /&gt;
		} );&lt;br /&gt;
		revs.sort( ( a, b ) =&amp;gt; ( a.timestamp &amp;lt; b.timestamp ? -1 : a.timestamp &amp;gt; b.timestamp ? 1 : a.id - b.id ) );&lt;br /&gt;
		return revs;&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function fetchExport() {&lt;br /&gt;
		const url = mw.util.getUrl( &amp;#039;Special:Export/&amp;#039; + config.wgPageName, { history: 1 } );&lt;br /&gt;
		return fetch( url, { credentials: &amp;#039;same-origin&amp;#039; } ).then( ( r ) =&amp;gt; {&lt;br /&gt;
			if ( !r.ok ) {&lt;br /&gt;
				throw new Error( &amp;#039;blame-export-http-&amp;#039; + r.status );&lt;br /&gt;
			}&lt;br /&gt;
			return r.text();&lt;br /&gt;
		} ).then( parseExport );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function fetchViaApi() {&lt;br /&gt;
		const api = new mw.Api();&lt;br /&gt;
		const revs = [];&lt;br /&gt;
		function batch( cont ) {&lt;br /&gt;
			const params = Object.assign( {&lt;br /&gt;
				action: &amp;#039;query&amp;#039;,&lt;br /&gt;
				prop: &amp;#039;revisions&amp;#039;,&lt;br /&gt;
				titles: config.wgPageName,&lt;br /&gt;
				rvprop: &amp;#039;ids|timestamp|user|comment|sha1|content&amp;#039;,&lt;br /&gt;
				rvslots: &amp;#039;main&amp;#039;,&lt;br /&gt;
				rvlimit: 50,&lt;br /&gt;
				rvdir: &amp;#039;newer&amp;#039;,&lt;br /&gt;
				formatversion: 2&lt;br /&gt;
			}, cont || {} );&lt;br /&gt;
			return api.get( params ).then( ( data ) =&amp;gt; {&lt;br /&gt;
				const p = data.query.pages[ 0 ];&lt;br /&gt;
				( p.revisions || [] ).forEach( ( r ) =&amp;gt; {&lt;br /&gt;
					const slot = r.slots &amp;amp;&amp;amp; r.slots.main;&lt;br /&gt;
					revs.push( {&lt;br /&gt;
						id: r.revid,&lt;br /&gt;
						parentid: r.parentid || 0,&lt;br /&gt;
						timestamp: r.timestamp,&lt;br /&gt;
						user: r.user || &amp;#039;&amp;#039;,&lt;br /&gt;
						comment: r.comment || &amp;#039;&amp;#039;,&lt;br /&gt;
						sha1: r.sha1 || &amp;#039;&amp;#039;,&lt;br /&gt;
						text: slot &amp;amp;&amp;amp; !slot.texthidden &amp;amp;&amp;amp; !r.texthidden &amp;amp;&amp;amp;&lt;br /&gt;
							typeof slot.content === &amp;#039;string&amp;#039; ? slot.content : null&lt;br /&gt;
					} );&lt;br /&gt;
				} );&lt;br /&gt;
				return data.continue ? batch( data.continue ) : revs;&lt;br /&gt;
			} );&lt;br /&gt;
		}&lt;br /&gt;
		return batch();&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function loadHistory() {&lt;br /&gt;
		if ( !historyPromise ) {&lt;br /&gt;
			historyPromise = fetchExport()&lt;br /&gt;
				.catch( fetchViaApi )&lt;br /&gt;
				.then( ( revs ) =&amp;gt; (&lt;br /&gt;
					// $wgExportAllowHistory=false: a one-revision export with a parent.&lt;br /&gt;
					revs.length === 1 &amp;amp;&amp;amp; revs[ 0 ].parentid &amp;gt; 0 ? fetchViaApi() : revs&lt;br /&gt;
				) )&lt;br /&gt;
				.catch( ( e ) =&amp;gt; {&lt;br /&gt;
					historyPromise = null;&lt;br /&gt;
					throw e;&lt;br /&gt;
				} );&lt;br /&gt;
		}&lt;br /&gt;
		return historyPromise;&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function setBody( body, children ) {&lt;br /&gt;
		body.textContent = &amp;#039;&amp;#039;;&lt;br /&gt;
		children.forEach( ( c ) =&amp;gt; c &amp;amp;&amp;amp; body.appendChild( c ) );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function loading( body, label ) {&lt;br /&gt;
		setBody( body, [&lt;br /&gt;
			el( &amp;#039;div&amp;#039;, { class: &amp;#039;gadget-blame__bar&amp;#039;, role: &amp;#039;progressbar&amp;#039;, &amp;#039;aria-label&amp;#039;: label } ),&lt;br /&gt;
			el( &amp;#039;div&amp;#039;, { class: &amp;#039;gadget-blame__meta&amp;#039;, text: label } )&lt;br /&gt;
		] );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function renderError( body, err ) {&lt;br /&gt;
		setBody( body, [ message( &amp;#039;error&amp;#039;, [&lt;br /&gt;
			&amp;#039;Could not load the page history (&amp;#039; + ( err &amp;amp;&amp;amp; err.message ? err.message : &amp;#039;unknown error&amp;#039; ) + &amp;#039;).&amp;#039;&lt;br /&gt;
		] ) ] );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	// The popover attribute gives light dismissal, Escape and the top layer. Only&lt;br /&gt;
	// placement is ours: CSS anchor positioning would do it declaratively but is&lt;br /&gt;
	// not yet in Firefox or Safari.&lt;br /&gt;
	let popoverEl = null;&lt;br /&gt;
	let popoverAnchor = null;&lt;br /&gt;
&lt;br /&gt;
	function setActive( on ) {&lt;br /&gt;
		if ( popoverAnchor ) {&lt;br /&gt;
			popoverAnchor.classList.toggle( &amp;#039;gadget-blame-w--active&amp;#039;, on );&lt;br /&gt;
		}&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function place() {&lt;br /&gt;
		if ( !popoverEl || !popoverAnchor ) {&lt;br /&gt;
			return;&lt;br /&gt;
		}&lt;br /&gt;
		const gap = 8;&lt;br /&gt;
		const a = popoverAnchor.getBoundingClientRect();&lt;br /&gt;
		const p = popoverEl.getBoundingClientRect();&lt;br /&gt;
		let top = a.bottom + gap;&lt;br /&gt;
		if ( top + p.height &amp;gt; window.innerHeight - gap &amp;amp;&amp;amp; a.top - gap - p.height &amp;gt; gap ) {&lt;br /&gt;
			top = a.top - gap - p.height;&lt;br /&gt;
		}&lt;br /&gt;
		const left = Math.max( gap, Math.min( a.left, window.innerWidth - p.width - gap ) );&lt;br /&gt;
		popoverEl.style.top = Math.max( gap, top ) + &amp;#039;px&amp;#039;;&lt;br /&gt;
		popoverEl.style.left = left + &amp;#039;px&amp;#039;;&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function hidePopover() {&lt;br /&gt;
		if ( !popoverEl ) {&lt;br /&gt;
			return;&lt;br /&gt;
		}&lt;br /&gt;
		if ( popoverEl.hidePopover ) {&lt;br /&gt;
			if ( popoverEl.matches( &amp;#039;:popover-open&amp;#039; ) ) {&lt;br /&gt;
				popoverEl.hidePopover();&lt;br /&gt;
			}&lt;br /&gt;
		} else {&lt;br /&gt;
			popoverEl.classList.remove( &amp;#039;gadget-blame-popover--open&amp;#039; );&lt;br /&gt;
		}&lt;br /&gt;
		setActive( false );&lt;br /&gt;
		popoverAnchor = null;&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function ensurePopoverEl() {&lt;br /&gt;
		if ( popoverEl ) {&lt;br /&gt;
			return popoverEl;&lt;br /&gt;
		}&lt;br /&gt;
		popoverEl = el( &amp;#039;div&amp;#039;, { class: &amp;#039;gadget-blame-popover&amp;#039;, role: &amp;#039;dialog&amp;#039;, &amp;#039;aria-label&amp;#039;: &amp;#039;Revision&amp;#039; } );&lt;br /&gt;
		if ( &amp;#039;popover&amp;#039; in popoverEl ) {&lt;br /&gt;
			popoverEl.setAttribute( &amp;#039;popover&amp;#039;, &amp;#039;auto&amp;#039; );&lt;br /&gt;
			// Light dismissal bypasses hidePopover, so the ring is cleared here.&lt;br /&gt;
			popoverEl.addEventListener( &amp;#039;toggle&amp;#039;, ( e ) =&amp;gt; {&lt;br /&gt;
				if ( e.newState === &amp;#039;closed&amp;#039; ) {&lt;br /&gt;
					setActive( false );&lt;br /&gt;
					popoverAnchor = null;&lt;br /&gt;
				}&lt;br /&gt;
			} );&lt;br /&gt;
		} else {&lt;br /&gt;
			document.addEventListener( &amp;#039;click&amp;#039;, ( e ) =&amp;gt; {&lt;br /&gt;
				if ( popoverAnchor &amp;amp;&amp;amp; !popoverEl.contains( e.target ) &amp;amp;&amp;amp; !e.target.closest( &amp;#039;.gadget-blame-w&amp;#039; ) ) {&lt;br /&gt;
					hidePopover();&lt;br /&gt;
				}&lt;br /&gt;
			} );&lt;br /&gt;
			document.addEventListener( &amp;#039;keydown&amp;#039;, ( e ) =&amp;gt; {&lt;br /&gt;
				if ( e.key === &amp;#039;Escape&amp;#039; &amp;amp;&amp;amp; popoverAnchor ) {&lt;br /&gt;
					hidePopover();&lt;br /&gt;
				}&lt;br /&gt;
			} );&lt;br /&gt;
		}&lt;br /&gt;
		// The anchor is inline text, so it moves with the page.&lt;br /&gt;
		window.addEventListener( &amp;#039;scroll&amp;#039;, place, true );&lt;br /&gt;
		window.addEventListener( &amp;#039;resize&amp;#039;, place );&lt;br /&gt;
		teleportTarget.appendChild( popoverEl );&lt;br /&gt;
		return popoverEl;&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function card( rev ) {&lt;br /&gt;
		if ( !rev ) {&lt;br /&gt;
			return [ message( &amp;#039;notice&amp;#039;, [ &amp;#039;Not written on this page — template or module output.&amp;#039; ] ) ];&lt;br /&gt;
		}&lt;br /&gt;
		const isIp = mw.util.isIPAddress ? mw.util.isIPAddress( rev.user ) : false;&lt;br /&gt;
		const who = rev.user ?&lt;br /&gt;
			link( mw.util.getUrl( ( isIp ? &amp;#039;Special:Contributions/&amp;#039; : &amp;#039;User:&amp;#039; ) + rev.user ), rev.user, &amp;#039;gadget-blame__user&amp;#039; ) :&lt;br /&gt;
			el( &amp;#039;span&amp;#039;, { class: &amp;#039;gadget-blame__meta&amp;#039;, text: HIDDEN } );&lt;br /&gt;
		return [ el( &amp;#039;div&amp;#039;, { class: &amp;#039;gadget-blame__card&amp;#039; }, [&lt;br /&gt;
			el( &amp;#039;div&amp;#039;, { class: &amp;#039;gadget-blame__date&amp;#039;, text: formatDate( rev.timestamp ) } ),&lt;br /&gt;
			el( &amp;#039;div&amp;#039;, null, [ who ] ),&lt;br /&gt;
			rev.comment ? el( &amp;#039;div&amp;#039;, { class: &amp;#039;gadget-blame__summary&amp;#039;, text: rev.comment } ) : null,&lt;br /&gt;
			el( &amp;#039;div&amp;#039;, { class: &amp;#039;gadget-blame__links&amp;#039; }, [&lt;br /&gt;
				link( mw.util.getUrl( &amp;#039;Special:Diff/&amp;#039; + rev.id ), &amp;#039;Diff&amp;#039; ),&lt;br /&gt;
				link( mw.util.getUrl( &amp;#039;Special:PermanentLink/&amp;#039; + rev.id ), &amp;#039;Revision &amp;#039; + rev.id )&lt;br /&gt;
			] )&lt;br /&gt;
		] ) ];&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function showPopover( anchor, rev ) {&lt;br /&gt;
		const node = ensurePopoverEl();&lt;br /&gt;
		setActive( false );&lt;br /&gt;
		popoverAnchor = anchor;&lt;br /&gt;
		setBody( node, card( rev ) );&lt;br /&gt;
		if ( node.showPopover ) {&lt;br /&gt;
			if ( node.matches( &amp;#039;:popover-open&amp;#039; ) ) {&lt;br /&gt;
				node.hidePopover();&lt;br /&gt;
			}&lt;br /&gt;
			node.showPopover();&lt;br /&gt;
		} else {&lt;br /&gt;
			node.classList.add( &amp;#039;gadget-blame-popover--open&amp;#039; );&lt;br /&gt;
		}&lt;br /&gt;
		setActive( true );&lt;br /&gt;
		place();&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	// Text is gathered per block so a word split across inline markup&lt;br /&gt;
	// (&amp;quot;&amp;lt;i&amp;gt;Title&amp;lt;/i&amp;gt;&amp;#039;s&amp;quot;) is tokenised whole rather than as three fragments.&lt;br /&gt;
	const BLOCKS = &amp;#039;address, article, aside, blockquote, caption, dd, details, dialog, div, dl, dt, fieldset, figcaption, figure, footer, form, h1, h2, h3, h4, h5, h6, header, hgroup, li, main, nav, ol, p, pre, section, summary, table, tbody, td, tfoot, th, thead, tr, ul&amp;#039;;&lt;br /&gt;
&lt;br /&gt;
	// A word straddling an inline element has more than one text-node range.&lt;br /&gt;
	function renderedTokens( root ) {&lt;br /&gt;
		const walker = document.createTreeWalker( root, NodeFilter.SHOW_TEXT, {&lt;br /&gt;
			acceptNode: ( n ) =&amp;gt; {&lt;br /&gt;
				const p = n.parentElement;&lt;br /&gt;
				return p &amp;amp;&amp;amp; !p.closest( UNPAINTED ) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT;&lt;br /&gt;
			}&lt;br /&gt;
		} );&lt;br /&gt;
		const re = new RegExp( text.TOKEN_RE.source, text.TOKEN_RE.flags );&lt;br /&gt;
		const out = [];&lt;br /&gt;
		let block = null;&lt;br /&gt;
		let joined = &amp;#039;&amp;#039;;&lt;br /&gt;
		let segments = [];&lt;br /&gt;
&lt;br /&gt;
		function flush() {&lt;br /&gt;
			if ( !joined ) {&lt;br /&gt;
				return;&lt;br /&gt;
			}&lt;br /&gt;
			re.lastIndex = 0;&lt;br /&gt;
			let m;&lt;br /&gt;
			let seg = 0;&lt;br /&gt;
			while ( ( m = re.exec( joined ) ) ) {&lt;br /&gt;
				const norm = text.normaliseText( m[ 0 ] );&lt;br /&gt;
				if ( !norm ) {&lt;br /&gt;
					continue;&lt;br /&gt;
				}&lt;br /&gt;
				const from = m.index;&lt;br /&gt;
				const to = from + m[ 0 ].length;&lt;br /&gt;
				while ( seg &amp;lt; segments.length &amp;amp;&amp;amp; segments[ seg ].end &amp;lt;= from ) {&lt;br /&gt;
					seg++;&lt;br /&gt;
				}&lt;br /&gt;
				const pieces = [];&lt;br /&gt;
				for ( let i = seg; i &amp;lt; segments.length &amp;amp;&amp;amp; segments[ i ].start &amp;lt; to; i++ ) {&lt;br /&gt;
					const sgm = segments[ i ];&lt;br /&gt;
					pieces.push( {&lt;br /&gt;
						node: sgm.node,&lt;br /&gt;
						start: Math.max( from, sgm.start ) - sgm.start,&lt;br /&gt;
						end: Math.min( to, sgm.end ) - sgm.start&lt;br /&gt;
					} );&lt;br /&gt;
				}&lt;br /&gt;
				out.push( { pieces, norm } );&lt;br /&gt;
			}&lt;br /&gt;
			joined = &amp;#039;&amp;#039;;&lt;br /&gt;
			segments = [];&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		let n;&lt;br /&gt;
		while ( ( n = walker.nextNode() ) ) {&lt;br /&gt;
			const owner = n.parentElement.closest( BLOCKS ) || root;&lt;br /&gt;
			if ( owner !== block ) {&lt;br /&gt;
				flush();&lt;br /&gt;
				block = owner;&lt;br /&gt;
			}&lt;br /&gt;
			segments.push( { node: n, start: joined.length, end: joined.length + n.nodeValue.length } );&lt;br /&gt;
			joined += n.nodeValue;&lt;br /&gt;
		}&lt;br /&gt;
		flush();&lt;br /&gt;
		return out;&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	// One replacement per text node; the original is kept for unpaint().&lt;br /&gt;
	function paint( tokens, classify ) {&lt;br /&gt;
		// A span cannot straddle text nodes, so a word split across inline markup&lt;br /&gt;
		// is painted once per node, each piece carrying the same origin.&lt;br /&gt;
		const byNode = new Map();&lt;br /&gt;
		tokens.forEach( ( t, k ) =&amp;gt; {&lt;br /&gt;
			const c = classify( k );&lt;br /&gt;
			t.pieces.forEach( ( piece ) =&amp;gt; {&lt;br /&gt;
				if ( !byNode.has( piece.node ) ) {&lt;br /&gt;
					byNode.set( piece.node, [] );&lt;br /&gt;
				}&lt;br /&gt;
				byNode.get( piece.node ).push( { start: piece.start, end: piece.end, c } );&lt;br /&gt;
			} );&lt;br /&gt;
		} );&lt;br /&gt;
		byNode.forEach( ( pieces, node ) =&amp;gt; {&lt;br /&gt;
			const s = node.nodeValue;&lt;br /&gt;
			const frag = document.createDocumentFragment();&lt;br /&gt;
			const added = [];&lt;br /&gt;
			let pos = 0;&lt;br /&gt;
			let k = 0;&lt;br /&gt;
			while ( k &amp;lt; pieces.length ) {&lt;br /&gt;
				const c = pieces[ k ].c;&lt;br /&gt;
				const key = c ? c.rev : -1;&lt;br /&gt;
				const start = pieces[ k ].start;&lt;br /&gt;
				let end = pieces[ k ].end;&lt;br /&gt;
				let next = k + 1;&lt;br /&gt;
				while ( next &amp;lt; pieces.length ) {&lt;br /&gt;
					const cn = pieces[ next ].c;&lt;br /&gt;
					if ( ( cn ? cn.rev : -1 ) !== key ) {&lt;br /&gt;
						break;&lt;br /&gt;
					}&lt;br /&gt;
					end = pieces[ next ].end;&lt;br /&gt;
					next++;&lt;br /&gt;
				}&lt;br /&gt;
				if ( start &amp;gt; pos ) {&lt;br /&gt;
					added.push( frag.appendChild( document.createTextNode( s.slice( pos, start ) ) ) );&lt;br /&gt;
				}&lt;br /&gt;
				const span = el( &amp;#039;span&amp;#039;, {&lt;br /&gt;
					class: &amp;#039;gadget-blame-w &amp;#039; + ( c ? &amp;#039;gadget-blame-a&amp;#039; + c.cls : &amp;#039;gadget-blame-w--template&amp;#039; ),&lt;br /&gt;
					text: s.slice( start, end )&lt;br /&gt;
				} );&lt;br /&gt;
				if ( c ) {&lt;br /&gt;
					span.setAttribute( &amp;#039;data-gadget-blame-revid&amp;#039;, String( c.rev ) );&lt;br /&gt;
					// So attribution is not carried by colour alone.&lt;br /&gt;
					span.setAttribute( &amp;#039;title&amp;#039;, c.who );&lt;br /&gt;
				}&lt;br /&gt;
				added.push( frag.appendChild( span ) );&lt;br /&gt;
				pos = end;&lt;br /&gt;
				k = next;&lt;br /&gt;
			}&lt;br /&gt;
			if ( pos &amp;lt; s.length ) {&lt;br /&gt;
				added.push( frag.appendChild( document.createTextNode( s.slice( pos ) ) ) );&lt;br /&gt;
			}&lt;br /&gt;
			node.parentNode.replaceChild( frag, node );&lt;br /&gt;
			page.painted.push( { original: node, pieces: added } );&lt;br /&gt;
		} );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	// Roving tabindex: thousands of tab stops would make the article unusable.&lt;br /&gt;
	// The runs deliberately carry no role either — one would make a screen reader&lt;br /&gt;
	// announce every phrase as a control instead of reading it as prose.&lt;br /&gt;
	function runs() {&lt;br /&gt;
		const root = contentRoot();&lt;br /&gt;
		return root ? Array.from( root.querySelectorAll( &amp;#039;.gadget-blame-w&amp;#039; ) ) : [];&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function setTabStop( run ) {&lt;br /&gt;
		runs().forEach( ( r ) =&amp;gt; r.setAttribute( &amp;#039;tabindex&amp;#039;, r === run ? &amp;#039;0&amp;#039; : &amp;#039;-1&amp;#039; ) );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function moveTabStop( from, delta ) {&lt;br /&gt;
		const all = runs();&lt;br /&gt;
		const i = all.indexOf( from );&lt;br /&gt;
		const next = all[ Math.min( all.length - 1, Math.max( 0, ( i &amp;lt; 0 ? 0 : i ) + delta ) ) ];&lt;br /&gt;
		if ( next ) {&lt;br /&gt;
			setTabStop( next );&lt;br /&gt;
			next.focus();&lt;br /&gt;
			next.scrollIntoView( { block: &amp;#039;nearest&amp;#039; } );&lt;br /&gt;
		}&lt;br /&gt;
		return next;&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function unpaint() {&lt;br /&gt;
		page.painted.forEach( ( p ) =&amp;gt; {&lt;br /&gt;
			const first = p.pieces[ 0 ];&lt;br /&gt;
			if ( first &amp;amp;&amp;amp; first.parentNode ) {&lt;br /&gt;
				first.parentNode.insertBefore( p.original, first );&lt;br /&gt;
			}&lt;br /&gt;
			p.pieces.forEach( ( n ) =&amp;gt; n.parentNode &amp;amp;&amp;amp; n.parentNode.removeChild( n ) );&lt;br /&gt;
		} );&lt;br /&gt;
		page.painted = [];&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function closeLegend() {&lt;br /&gt;
		if ( page.legend ) {&lt;br /&gt;
			page.legend.remove();&lt;br /&gt;
			page.legend = null;&lt;br /&gt;
		}&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function turnOff() {&lt;br /&gt;
		page.token++;&lt;br /&gt;
		page.on = false;&lt;br /&gt;
		unpaint();&lt;br /&gt;
		closeLegend();&lt;br /&gt;
		page.revById = null;&lt;br /&gt;
		hidePopover();&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function openLegend() {&lt;br /&gt;
		closeLegend();&lt;br /&gt;
		const body = el( &amp;#039;div&amp;#039;, { class: &amp;#039;gadget-blame__body&amp;#039; } );&lt;br /&gt;
		const off = iconButton( &amp;#039;Turn off&amp;#039;, ICON_CLOSE );&lt;br /&gt;
		off.addEventListener( &amp;#039;click&amp;#039;, turnOff );&lt;br /&gt;
		page.legend = el( &amp;#039;div&amp;#039;, { class: &amp;#039;gadget-blame-legend&amp;#039;, role: &amp;#039;region&amp;#039;, &amp;#039;aria-label&amp;#039;: &amp;#039;Blame legend&amp;#039; }, [&lt;br /&gt;
			el( &amp;#039;div&amp;#039;, { class: &amp;#039;gadget-blame__header&amp;#039; }, [&lt;br /&gt;
				el( &amp;#039;span&amp;#039;, { class: &amp;#039;gadget-blame__title&amp;#039;, text: &amp;#039;Blame&amp;#039; } ),&lt;br /&gt;
				off&lt;br /&gt;
			] ),&lt;br /&gt;
			body&lt;br /&gt;
		] );&lt;br /&gt;
		teleportTarget.appendChild( page.legend );&lt;br /&gt;
		return body;&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function pct( n, total ) {&lt;br /&gt;
		if ( !total || !n ) {&lt;br /&gt;
			return &amp;#039;0%&amp;#039;;&lt;br /&gt;
		}&lt;br /&gt;
		const p = Math.round( 100 * n / total );&lt;br /&gt;
		return p === 0 ? &amp;#039;&amp;lt;1%&amp;#039; : p + &amp;#039;%&amp;#039;;&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function legendRow( swatchClass, label, share ) {&lt;br /&gt;
		const labelNode = typeof label === &amp;#039;string&amp;#039; ? el( &amp;#039;span&amp;#039;, { text: label } ) : label;&lt;br /&gt;
		labelNode.classList.add( &amp;#039;gadget-blame__label&amp;#039; );&lt;br /&gt;
		return el( &amp;#039;div&amp;#039;, { class: &amp;#039;gadget-blame__row&amp;#039; }, [&lt;br /&gt;
			el( &amp;#039;span&amp;#039;, { class: &amp;#039;gadget-blame-swatch &amp;#039; + swatchClass } ),&lt;br /&gt;
			labelNode,&lt;br /&gt;
			el( &amp;#039;span&amp;#039;, { class: &amp;#039;gadget-blame__meta&amp;#039;, text: share } )&lt;br /&gt;
		] );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function plural( n, word ) {&lt;br /&gt;
		return n + &amp;#039; &amp;#039; + word + ( n === 1 ? &amp;#039;&amp;#039; : &amp;#039;s&amp;#039; );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function renderLegend( body, stats ) {&lt;br /&gt;
		const rows = stats.authors.slice( 0, PALETTE_SIZE ).map( ( pair, idx ) =&amp;gt; legendRow(&lt;br /&gt;
			&amp;#039;gadget-blame-a&amp;#039; + idx,&lt;br /&gt;
			userLink( pair[ 0 ] === HIDDEN ? &amp;#039;&amp;#039; : pair[ 0 ] ),&lt;br /&gt;
			pct( pair[ 1 ], stats.total )&lt;br /&gt;
		) );&lt;br /&gt;
		const rest = stats.authors.slice( PALETTE_SIZE );&lt;br /&gt;
		if ( rest.length ) {&lt;br /&gt;
			const n = rest.reduce( ( sum, pair ) =&amp;gt; sum + pair[ 1 ], 0 );&lt;br /&gt;
			rows.push( legendRow( &amp;#039;gadget-blame-ax&amp;#039;, plural( rest.length, &amp;#039;other editor&amp;#039; ), pct( n, stats.total ) ) );&lt;br /&gt;
		}&lt;br /&gt;
		if ( stats.template ) {&lt;br /&gt;
			rows.push( legendRow( &amp;#039;gadget-blame-w--template&amp;#039;, &amp;#039;Template output&amp;#039;, pct( stats.template, stats.total ) ) );&lt;br /&gt;
		}&lt;br /&gt;
		const notes = [ plural( stats.revisions, &amp;#039;revision&amp;#039; ) + &amp;#039; blamed&amp;#039; ];&lt;br /&gt;
		if ( stats.reused ) {&lt;br /&gt;
			notes.push( plural( stats.reused, &amp;#039;revision&amp;#039; ) + &amp;#039; restored an earlier version&amp;#039; );&lt;br /&gt;
		}&lt;br /&gt;
		if ( stats.capped ) {&lt;br /&gt;
			notes.push( plural( stats.capped, &amp;#039;revision&amp;#039; ) + &amp;#039; changed too much to trace word by word&amp;#039; );&lt;br /&gt;
		}&lt;br /&gt;
		if ( stats.gaps ) {&lt;br /&gt;
			notes.push( &amp;#039;text older than this history, or from a hidden revision, is credited to the oldest revision that shows it&amp;#039; );&lt;br /&gt;
		}&lt;br /&gt;
		if ( stats.alignCapped ) {&lt;br /&gt;
			notes.push( &amp;#039;this page and its source differ too much to line up, so nothing could be attributed&amp;#039; );&lt;br /&gt;
		}&lt;br /&gt;
		rows.push( el( &amp;#039;div&amp;#039;, {&lt;br /&gt;
			class: &amp;#039;gadget-blame__footer&amp;#039;,&lt;br /&gt;
			text: notes.join( &amp;#039;. &amp;#039; ) + &amp;#039;. Click a word for its revision, or tab into the text and use the arrow keys.&amp;#039;&lt;br /&gt;
		} ) );&lt;br /&gt;
		setBody( body, rows );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	// One revision at a time: only the previous token stream is ever needed, and&lt;br /&gt;
	// holding every stream at once runs to hundreds of megabytes.&lt;br /&gt;
	async function buildOrigins( revs, body, token ) {&lt;br /&gt;
		const pageName = config.wgPageName.replace( /_/g, &amp;#039; &amp;#039; );&lt;br /&gt;
		const blamer = text.makeBlamer(&lt;br /&gt;
			text.duplicateSha1s( revs.map( ( r ) =&amp;gt; r.sha1 ) ),&lt;br /&gt;
			MAX_D_REVISION&lt;br /&gt;
		);&lt;br /&gt;
		const intern = text.makeInterner();&lt;br /&gt;
		let reused = 0;&lt;br /&gt;
		let slice = performance.now();&lt;br /&gt;
		for ( let i = 0; i &amp;lt; revs.length; i++ ) {&lt;br /&gt;
			const r = revs[ i ];&lt;br /&gt;
			if ( blamer.reuse( r.sha1 ) ) {&lt;br /&gt;
				reused++;&lt;br /&gt;
			} else {&lt;br /&gt;
				blamer.push( intern( text.tokenize( text.wikitextToText( r.text, { pageName } ) ) ), i, r.sha1 );&lt;br /&gt;
			}&lt;br /&gt;
			if ( performance.now() - slice &amp;gt; SLICE_MS ) {&lt;br /&gt;
				loading( body, &amp;#039;Blaming &amp;#039; + ( i + 1 ) + &amp;#039; of &amp;#039; + revs.length + &amp;#039; revisions…&amp;#039; );&lt;br /&gt;
				await tick();&lt;br /&gt;
				if ( token !== page.token ) {&lt;br /&gt;
					return null;&lt;br /&gt;
				}&lt;br /&gt;
				slice = performance.now();&lt;br /&gt;
			}&lt;br /&gt;
		}&lt;br /&gt;
		return { intern, origins: blamer.origins, stream: blamer.stream, capped: blamer.capped, reused };&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	async function blamePage() {&lt;br /&gt;
		if ( page.on ) {&lt;br /&gt;
			turnOff();&lt;br /&gt;
			return;&lt;br /&gt;
		}&lt;br /&gt;
		const root = contentRoot();&lt;br /&gt;
		if ( !root ) {&lt;br /&gt;
			return;&lt;br /&gt;
		}&lt;br /&gt;
		page.on = true;&lt;br /&gt;
		const token = ++page.token;&lt;br /&gt;
		const body = openLegend();&lt;br /&gt;
		loading( body, &amp;#039;Fetching history…&amp;#039; );&lt;br /&gt;
		try {&lt;br /&gt;
			const all = await loadHistory();&lt;br /&gt;
			if ( token !== page.token ) {&lt;br /&gt;
				return;&lt;br /&gt;
			}&lt;br /&gt;
			const upTo = config.wgRevisionId || config.wgCurRevisionId || Infinity;&lt;br /&gt;
			const visible = all.filter( ( r ) =&amp;gt; r.id &amp;lt;= upTo );&lt;br /&gt;
			const revs = visible.filter( ( r ) =&amp;gt; r.text !== null );&lt;br /&gt;
			if ( !revs.length ) {&lt;br /&gt;
				setBody( body, [ message( &amp;#039;warning&amp;#039;, [ &amp;#039;No readable revisions to blame on this page.&amp;#039; ] ) ] );&lt;br /&gt;
				return;&lt;br /&gt;
			}&lt;br /&gt;
			// Text this history cannot account for: revisions hidden inside it, or a&lt;br /&gt;
			// history not starting at the page&amp;#039;s creation.&lt;br /&gt;
			const gaps = ( visible.length - revs.length ) + ( revs[ 0 ].parentid &amp;gt; 0 ? 1 : 0 );&lt;br /&gt;
			const built = await buildOrigins( revs, body, token );&lt;br /&gt;
			if ( !built ) {&lt;br /&gt;
				return;&lt;br /&gt;
			}&lt;br /&gt;
			const toks = renderedTokens( root );&lt;br /&gt;
			const alignStats = {};&lt;br /&gt;
			const norms = toks.map( ( t ) =&amp;gt; t.norm );&lt;br /&gt;
			const map = text.dropPunctuationOnlyRuns(&lt;br /&gt;
				text.diffMap( built.stream, built.intern( norms ), MAX_D_ALIGN, alignStats ),&lt;br /&gt;
				norms&lt;br /&gt;
			);&lt;br /&gt;
			const counts = new Map();&lt;br /&gt;
			let aligned = 0;&lt;br /&gt;
			for ( let k = 0; k &amp;lt; toks.length; k++ ) {&lt;br /&gt;
				if ( map[ k ] &amp;gt;= 0 ) {&lt;br /&gt;
					aligned++;&lt;br /&gt;
					const u = revs[ built.origins[ map[ k ] ] ].user || HIDDEN;&lt;br /&gt;
					counts.set( u, ( counts.get( u ) || 0 ) + 1 );&lt;br /&gt;
				}&lt;br /&gt;
			}&lt;br /&gt;
			const authors = Array.from( counts.entries() ).sort( ( a, b ) =&amp;gt; b[ 1 ] - a[ 1 ] );&lt;br /&gt;
			const cls = new Map();&lt;br /&gt;
			authors.forEach( ( pair, idx ) =&amp;gt; cls.set( pair[ 0 ], idx &amp;lt; PALETTE_SIZE ? String( idx ) : &amp;#039;x&amp;#039; ) );&lt;br /&gt;
			page.revById = new Map( revs.map( ( r ) =&amp;gt; [ r.id, r ] ) );&lt;br /&gt;
			paint( toks, ( k ) =&amp;gt; {&lt;br /&gt;
				const w = map[ k ];&lt;br /&gt;
				if ( w &amp;lt; 0 ) {&lt;br /&gt;
					return null;&lt;br /&gt;
				}&lt;br /&gt;
				const r = revs[ built.origins[ w ] ];&lt;br /&gt;
				return { cls: cls.get( r.user || HIDDEN ), rev: r.id, who: r.user || HIDDEN };&lt;br /&gt;
			} );&lt;br /&gt;
			setTabStop( toks.length ? contentRoot().querySelector( &amp;#039;.gadget-blame-w&amp;#039; ) : null );&lt;br /&gt;
			renderLegend( body, {&lt;br /&gt;
				authors,&lt;br /&gt;
				total: toks.length,&lt;br /&gt;
				template: toks.length - aligned,&lt;br /&gt;
				revisions: revs.length,&lt;br /&gt;
				reused: built.reused,&lt;br /&gt;
				capped: built.capped,&lt;br /&gt;
				alignCapped: !!alignStats.capped,&lt;br /&gt;
				gaps&lt;br /&gt;
			} );&lt;br /&gt;
			// Tens of megabytes on a long history; only popover metadata is kept.&lt;br /&gt;
			revs.forEach( ( r ) =&amp;gt; {&lt;br /&gt;
				r.text = null;&lt;br /&gt;
			} );&lt;br /&gt;
			historyPromise = null;&lt;br /&gt;
		} catch ( err ) {&lt;br /&gt;
			if ( token === page.token ) {&lt;br /&gt;
				renderError( body, err );&lt;br /&gt;
			}&lt;br /&gt;
		}&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function openRun( w ) {&lt;br /&gt;
		setTabStop( w );&lt;br /&gt;
		const id = w.getAttribute( &amp;#039;data-gadget-blame-revid&amp;#039; );&lt;br /&gt;
		showPopover( w, id === null ? null : page.revById.get( Number( id ) ) );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	// While blame is on, a run inside a link opens its revision instead of&lt;br /&gt;
	// following the link.&lt;br /&gt;
	document.addEventListener( &amp;#039;click&amp;#039;, ( e ) =&amp;gt; {&lt;br /&gt;
		const w = page.on &amp;amp;&amp;amp; e.target.closest &amp;amp;&amp;amp; e.target.closest( &amp;#039;.gadget-blame-w&amp;#039; );&lt;br /&gt;
		if ( !w ) {&lt;br /&gt;
			return;&lt;br /&gt;
		}&lt;br /&gt;
		if ( w.closest( &amp;#039;a&amp;#039; ) ) {&lt;br /&gt;
			e.preventDefault();&lt;br /&gt;
		}&lt;br /&gt;
		openRun( w );&lt;br /&gt;
	} );&lt;br /&gt;
&lt;br /&gt;
	document.addEventListener( &amp;#039;keydown&amp;#039;, ( e ) =&amp;gt; {&lt;br /&gt;
		const w = page.on &amp;amp;&amp;amp; e.target.closest &amp;amp;&amp;amp; e.target.closest( &amp;#039;.gadget-blame-w&amp;#039; );&lt;br /&gt;
		if ( !w || e.altKey || e.ctrlKey || e.metaKey ) {&lt;br /&gt;
			return;&lt;br /&gt;
		}&lt;br /&gt;
		if ( e.key === &amp;#039;ArrowRight&amp;#039; || e.key === &amp;#039;ArrowDown&amp;#039; ) {&lt;br /&gt;
			e.preventDefault();&lt;br /&gt;
			moveTabStop( w, 1 );&lt;br /&gt;
		} else if ( e.key === &amp;#039;ArrowLeft&amp;#039; || e.key === &amp;#039;ArrowUp&amp;#039; ) {&lt;br /&gt;
			e.preventDefault();&lt;br /&gt;
			moveTabStop( w, -1 );&lt;br /&gt;
		} else if ( e.key === &amp;#039;Enter&amp;#039; || e.key === &amp;#039; &amp;#039; ) {&lt;br /&gt;
			e.preventDefault();&lt;br /&gt;
			openRun( w );&lt;br /&gt;
		}&lt;br /&gt;
	} );&lt;br /&gt;
&lt;br /&gt;
	const item = mw.util.addPortletLink( &amp;#039;p-tb&amp;#039;, &amp;#039;#&amp;#039;, &amp;#039;Blame this page&amp;#039;, &amp;#039;gadget-blame-page&amp;#039;, &amp;#039;Colour every word by the editor who last changed it&amp;#039; );&lt;br /&gt;
	if ( item ) {&lt;br /&gt;
		item.addEventListener( &amp;#039;click&amp;#039;, ( e ) =&amp;gt; {&lt;br /&gt;
			e.preventDefault();&lt;br /&gt;
			blamePage();&lt;br /&gt;
		} );&lt;br /&gt;
	}&lt;br /&gt;
}() );&lt;/div&gt;</summary>
		<author><name>Kai</name></author>
	</entry>
</feed>