User:Aizag/interlangRefCount.js
外观
注意:保存之后,你必须清除浏览器缓存才能看到做出的更改。Google Chrome、Firefox、Microsoft Edge及Safari:按住⇧ Shift键并单击工具栏的“刷新”按钮。参阅Help:绕过浏览器缓存以获取更多帮助。
// 未經許可,請勿更改
// 在跨語言連結後面顯示該語言版本條目的引用總數
// 計數方式:該條目渲染 HTML 中 <ol class="references"> 底下的 <li> 數量
// 節流:並發 3、每分鐘最多 150 次請求、429 時依 Retry-After 等候重試;快取 24 小時
( function () {
'use strict';
var CACHE_PREFIX = 'ilRefCount:';
var CACHE_TTL = 24 * 60 * 60 * 1000; // 快取 24 小時
var CONCURRENCY = 3; // Wikimedia 官方建議 <= 3
var PER_MINUTE = 150; // 匿名上限 200/分,留安全餘量
var MAX_RETRY = 2;
if ( mw.config.get( 'wgNamespaceNumber' ) !== 0 ) {
return;
}
function readCache( key ) {
try {
var raw = localStorage.getItem( CACHE_PREFIX + key );
if ( !raw ) {
return null;
}
var obj = JSON.parse( raw );
if ( Date.now() - obj.t > CACHE_TTL ) {
return null;
}
return obj.n;
} catch ( e ) {
return null;
}
}
function writeCache( key, n ) {
try {
localStorage.setItem(
CACHE_PREFIX + key,
JSON.stringify( { t: Date.now(), n: n } )
);
} catch ( e ) {
// 忽略容量不足
}
}
function sleep( ms ) {
return new Promise( function ( resolve ) {
setTimeout( resolve, ms );
} );
}
// 每分鐘請求數閘門
var stamps = [];
function acquireSlot() {
var now = Date.now();
stamps = stamps.filter( function ( t ) {
return now - t < 60000;
} );
if ( stamps.length < PER_MINUTE ) {
stamps.push( now );
return Promise.resolve();
}
var wait = 60000 - ( now - stamps[ 0 ] ) + 50;
return sleep( wait ).then( acquireSlot );
}
function countRefs( html ) {
var doc = new DOMParser().parseFromString( html, 'text/html' );
var n = 0;
doc.querySelectorAll( 'ol.references' ).forEach( function ( ol ) {
n += ol.querySelectorAll( ':scope > li' ).length;
} );
return n;
}
function buildUrl( host, title ) {
return 'https://' + host + '/w/api.php?' + $.param( {
action: 'parse',
page: title,
prop: 'text',
formatversion: 2,
format: 'json',
disabletoc: 1,
disablelimitreport: 1,
disableeditsection: 1,
origin: '*'
} );
}
function fetchCount( host, title, attempt ) {
attempt = attempt || 0;
var url = buildUrl( host, title );
return acquireSlot().then( function () {
return fetch( url );
} ).then( function ( r ) {
if ( r.status === 429 ) {
if ( attempt >= MAX_RETRY ) {
throw new Error( '429' );
}
var ra = parseInt( r.headers.get( 'Retry-After' ), 10 );
var wait = ( isNaN( ra ) ? 20 : ra + 1 ) * 1000;
return sleep( wait ).then( function () {
return fetchCount( host, title, attempt + 1 );
} );
}
if ( !r.ok ) {
throw new Error( 'HTTP ' + r.status );
}
return r.json().then( function ( data ) {
if ( !data.parse ) {
throw new Error( 'no parse' );
}
return countRefs( data.parse.text );
} );
} );
}
function addBadge( link, text, title ) {
var badge = document.createElement( 'span' );
badge.className = 'il-refcount';
badge.style.cssText = 'margin-left:0.3em;font-size:85%;color:#54595d;';
badge.textContent = '(' + text + ')';
if ( title ) {
badge.title = title;
}
link.appendChild( badge );
return badge;
}
function run() {
var links = Array.prototype.slice.call(
document.querySelectorAll( 'a.interlanguage-link-target' )
);
if ( !links.length ) {
return;
}
var queue = links.map( function ( link ) {
var u;
try {
u = new URL( link.href );
} catch ( e ) {
return null;
}
var title = decodeURIComponent( u.pathname.replace( /^\/wiki\//, '' ) );
return {
link: link,
host: u.hostname,
title: title,
key: u.hostname + '|' + title
};
} ).filter( Boolean );
queue = queue.filter( function ( item ) {
var cached = readCache( item.key );
if ( cached !== null ) {
addBadge( item.link, cached, '引用數(快取)' );
return false;
}
item.badge = addBadge( item.link, '…', '載入中' );
return true;
} );
var active = 0;
function next() {
while ( active < CONCURRENCY && queue.length ) {
var item = queue.shift();
active++;
( function ( it ) {
fetchCount( it.host, it.title ).then( function ( n ) {
writeCache( it.key, n );
it.badge.textContent = '(' + n + ')';
it.badge.title = '引用數';
} ).catch( function ( e ) {
it.badge.textContent = '(?)';
it.badge.title = '無法取得:' + e.message;
} ).then( function () {
active--;
next();
} );
}( item ) );
}
}
next();
}
$( run );
}() );