User:PhiLiP/cgroup-finder.js
外观
注意:保存之后,你必须清除浏览器缓存才能看到做出的更改。Google Chrome、Firefox、Microsoft Edge及Safari:按住⇧ Shift键并单击工具栏的“刷新”按钮。参阅Help:绕过浏览器缓存以获取更多帮助。
/**
* CGroupFinder
*
* This script uses MediaWiki APIs to help find articles that lack of certain
* CGroups but may need them actually.
* Also see [[Wikipedia:字詞轉換處理/公共轉換組]].
*
* @author: [[User:PhiLiP]]
*/
var MWAPI = new mw.Api();
/**
* Core function to get sub-category through API
*
* @param {string} title The full category title (include the namespace)
* @param {integer} limit
* @param {string} _continue The correspond param of API's "cmcontinue",
* For internal usage.
* @return {jQuery#Promise object}
*
* The returning promise passes these parameters when resolved:
* @param {array[string]} titles
* @param {function|null} next()
* @param {string|null} currentContinue
* @param {string|null} nextContinue
*
* The next() function:
* @return {jQuery#Promise object} The same to getSubCategory
*/
function getSubCategory( category, limit, _continue ) {
return MWAPI
.post( {
action: 'query',
list: 'categorymembers',
cmtitle: category,
cmtype: 'subcat',
cmcontinue: _continue,
cmlimit: limit
} )
.then( function( data ) {
var members = 'query' in data ? data.query.categorymembers : [],
nextCont = 'continue' in data ? data['continue'].cmcontinue : null;
return $.Deferred().resolve(
/* titles = */$.map( members, function( page ) {
return page.title;
} ),
/* next = */nextCont && function() {
return getSubCategory(
category, limit, nextCont );
},
/* currentContinue = */_continue,
/* nextContinue = */nextCont ).promise();
} );
}
/**
* Core function to find articles in a category which do not transclude giving
* template. It is useful to find any article which should use but doesn't
* use certain CGroup(s).
*
* @param {string} category Full category title
* @param {string} template Full template title
* @param {integer} limit Not guaranteed
* @param {string} _continue Internal (API) continue string
* @return {jQuery#Promise object} The same to getSubCategory
*
* Except the next() function passed when resolved:
* @param {integer} limit Default to the previous limit
*/
function getNontransArticlesInCategory( category, template, limit, _continue ) {
var scopedLimit = limit;
// I didn't use generator here since JSON messes up the sort order when
// the result pages is in the form of Object (dictionary).
return MWAPI
.post( {
action: 'query',
list: 'categorymembers',
cmtitle: category,
cmtype: 'page',
cmcontinue: _continue,
cmlimit: limit
} )
.then( function( data ) {
var members = 'query' in data ? data.query.categorymembers : [],
pageids = $.map( members, function( m ) { return m.pageid; } ),
pageLength = pageids.length;
return ( pageLength ? MWAPI
.post( {
action: 'query',
prop: 'templates',
pageids: pageids.join( '|' ),
tltemplates: template,
tllimit: pageLength
} ) : $.Deferred().resolve( null ).promise() )
.then( function( d2 ) {
var titles,
nextCont =
'continue' in data ? data['continue'].cmcontinue : null;
if ( pageLength && 'query' in d2 ) {
titles = $.map( pageids, function( pageid ) {
var page = d2.query.pages[pageid];
if ( !( 'templates' in page ) ) {
return page.title;
}
else return null;
} );
} else {
titles = [];
}
return $.Deferred().resolve( titles, nextCont ).promise();
} );
} )
.then( function( titles, nextCont ) {
return $.Deferred().resolve(
titles,
nextCont && function( limit ) {
return getNontransArticlesInCategory(
category, template, limit || scopedLimit, nextCont );
},
_continue, nextCont ).promise();
} );
}
/**
* Core function to find category member articles lack of certain
* template which will also flatten the result for further UI rendering.
*
* @param {string} category Full category title
* @param {string} template Full template title
* @param {object} startAt Combination to resume from last position
* @param {object|null} categoryPath Internal object to store the search path
* @return {jQuery#Promise object} Gives title results and startAt combination
*
* The returning promise passes these parameters when resolved:
* @param {array[[string, string]]} titles_and_categories
* @param {object} startAt Same combination to flattenNontransArticles
* @param {boolean} end
*
*/
function flattenNontransArticles(
category, template, fetch, startAt, categoryPath ) {
var result = [],
_continue = startAt ? startAt['continue'] : null,
startPage = startAt ? startAt.startPage : null,
processedPath = startAt ? startAt.processedPath : {},
inProcessedPath = false,
deferred = $.Deferred();
function resolveEnd() {
deferred.resolve( result, /* startAt = */null, /* end = */true );
}
function processTitlesAndNext( titles, next, currentCont, nextCont ) {
var nextContinue,
stopPage = null;
$.each( titles, function( _, title ) {
if ( startPage ) {
if ( startPage === title ) {
startPage = null;
}
else return; // continue
}
if ( ( fetch -- ) > 0 ) {
result.push( [ title, category ] );
}
else { // fetch == -1
stopPage = title;
return false; // break
}
} );
if ( fetch > 0 && next instanceof Function ) {
return next( fetch ).then( processTitlesAndNext );
}
else if ( fetch === 0 ) {
// start a new cmcontinue of current category
nextContinue = nextCont;
}
else { // fetch === -1
// still in current cmcontinue
nextContinue = currentCont;
}
return $.Deferred().resolve( stopPage, nextContinue );
}
function processSubCategory( titles, next, parentCurCont, parentNextCont ) {
if ( titles.length === 0 ) {
if ( next instanceof Function ) {
// fetch more subcats in same level
categoryPath[category] = parentNextCont;
return next().then( processSubCategory );
}
// else, it's the end of current category
resolveEnd();
return deferred.promise();
}
var subCat = titles.shift(),
subCatPath = $.extend( {}, categoryPath );
// search pages in subcat from first page
subCatPath[subCat] = null;
// clean dangling data
if ( Object.keys( processedPath ).length === 0 ) {
_continue = startPage = null;
}
// start to search a new category
return flattenNontransArticles(
subCat, template, fetch, {
'continue': _continue,
startPage: startPage,
processedPath: processedPath
}, subCatPath )
.then( function( subResult, startAt ) {
// merge subcat result
fetch -= subResult.length;
$.merge( result, subResult );
if ( fetch === 0 ) {
// call resolved now
deferred.resolve( result, startAt, /* end = */false );
return deferred.promise();
}
else {
// not done yet. process remaining fetched sub-categories
return processSubCategory(
titles, next, parentCurCont, parentNextCont );
}
} );
}
function callProcessSubCategory() {
return getSubCategory(
category,
/* limit =*/10,
/* _continue = */categoryPath[category] )
.then( processSubCategory );
}
if ( !categoryPath ) {
// initial top
categoryPath = {};
categoryPath[category] = null;
}
if ( category in processedPath ) {
inProcessedPath = true;
// fast forward to stored continue
categoryPath[category] = processedPath[category];
delete processedPath[category];
}
if ( Object.keys( processedPath ).length === 0 ) {
// it's in the bottom of processedPath or it's just a new category
// never known. Either of them should fetch articles
getNontransArticlesInCategory(
category, template, fetch, _continue )
.then( processTitlesAndNext )
.then( function( nextStartPage, nextContinue ) {
_continue = startPage = null;
if ( fetch < 1 ) {
// we got all result we interested now.
// it's time to call resolved.
deferred.resolve( result, {
startPage: nextStartPage,
processedPath: categoryPath,
'continue': nextContinue
}, /* end = */false );
}
else {
// it's not enough in current category, search subcat now
return callProcessSubCategory();
}
} );
}
else if ( fetch > 0 && inProcessedPath ) {
// may have unprocessed sub-category
return callProcessSubCategory();
}
else {
// processed previously
resolveEnd();
}
return deferred.promise();
}
function normalizeCategoryTitle( category ) {
category = category.replace( /^(分类|分類|category):/i, 'Category:' );
if ( !( /^Category:/.test( category ) ) ) {
category = 'Category:' + category;
}
return category;
}
/**
* UI function to build main search form of CGroupFinder
*
* Requires these modules for correct style:
*
* - ext.inputBox.styles
* - mediawiki.ui.input
* - mediawiki.ui.button
*
* @param {string} pname Template to be searched around
*/
function uiSearchForm( tpl ) {
var shortName = tpl.replace( /^(Template|Module|模块):CGroup\//, '' );
var $button, $form = $( (
'<form class="mw-inputbox-centered cgroupfinder-form">$1 ' +
'<input name="category" type="text" placeholder="$2"' +
'class="mw-ui-input mw-ui-input-inline" size=30>' +
' <button type="submit" class="mw-ui-button mw-ui-progressive"' +
' disabled>$3</button></form>'
)
.replace(
'$1', mw.message( 'cgroupfinder-search-label', shortName ).escaped() )
.replace( '$2', mw.message( 'cgroupfinder-input-placeholder' ).escaped() )
.replace( '$3', mw.message( 'cgroupfinder-search' ).escaped() ) )
.data( 'cgroup-template', tpl );
$button = $form.find( 'button[type="submit"]' );
return $form.find( 'input[type="text"]' )
.on( 'keydown paste change', function( evt ) {
var self = this;
if ( $form.data( 'cgroupfinder-loading' ) ) {
evt.preventDefault();
return;
}
setTimeout( function() {
if ( !$form.data( 'cgroupfinder-loading' ) ) {
$button.prop( 'disabled', self.value.length === 0 );
}
} );
} ).end();
}
function uiSearchResultGroup( topCat, curCat ) {
var $group = $(
'<div class="cgroupfinder-search-result-group">' );
if ( topCat === curCat ) {
$group.append(
'<h3><a href="$1">$2</a></h3>'
.replace( '$1', mw.util.getUrl( curCat ) )
.replace( '$2', curCat.replace( /^Category:/, '' ) )
);
}
else {
$group.append(
'<h3>>> <a href="$1">$2</a></h3>'
.replace( '$1', mw.util.getUrl( curCat ) )
.replace( '$2', curCat.replace( /^Category:/, '' ) )
);
}
$group.append( '<ul class="cgroupfinder-search-result-list">' );
return $group;
}
function uiSearchResult( titles, topCat, curStartAt, nextStartAt, isEnd ) {
var $curGroup, lastCat, $curList,
$container = $( '<div class="cgroupfinder-search-result-container">' ),
$groups = $( '<div class="cgroupfinder-search-result-groups">' ),
$pagination = uiPagination( curStartAt, nextStartAt, isEnd );
$container
.append(
'<p class="cgroupfinder-search-result-description">$1</p>'
.replace(
'$1',
mw.message( 'cgroupfinder-search-result-description' ).escaped() )
)
.append( $pagination )
.append( $groups );
$.each( titles, function( _, title ) {
var $item,
category = title[1];
title = title[0];
if ( category !== lastCat ) {
$curGroup = uiSearchResultGroup( topCat, category )
.appendTo( $groups );
$curList = $curGroup.find( 'ul' );
lastCat = category;
}
$item = $( (
'<li class="cgroupfinder-search-result-item">' +
'<a href="$1">$2</a>' +
'<span class="cgroupfinder-search-result-item-options">' +
'</span></li>'
)
.replace( '$1', mw.util.getUrl( title ) )
.replace( '$2', title ) )
.appendTo( $curList );
} );
return $container;
}
var _paginations = [];
function uiPagination( curStartAt, nextStartAt, isEnd ) {
var prevStartAt = false;
if ( curStartAt !== null ) {
// page > 1
if ( curStartAt === _paginations[_paginations.length - 2] ) {
// clicked prev
_paginations.pop(); // pop orig current
if ( _paginations.length > 1 ) {
// still have prev
prevStartAt = _paginations[_paginations.length - 2];
}
// else prevStartAt should be false
}
else {
// clicked next, already have _paginations
prevStartAt = _paginations[_paginations.length - 1];
_paginations.push( curStartAt );
}
}
else {
// page = 1
_paginations = [ null ];
}
var $pagination = $( '<div class="cgroupfinder-pagination">' ),
$prev = $(
( prevStartAt !== false ? '<a href="#">$1</a>' : '<span>$1</span>' )
.replace(
'$1', mw.message( 'cgroupfinder-pagination-prev' ).escaped() )
),
$next = $(
( nextStartAt ? '<a href="#">$1</a>' : '<span>$1</span>' )
.replace(
'$1', mw.message( 'cgroupfinder-pagination-next' ).escaped() )
);
$pagination
.append( ' (' ).append( $prev ).append( ' | ' )
.append( $next ).append( ')' );
if ( prevStartAt !== false ) {
$prev.on( 'click', function( evt ) {
evt.preventDefault();
doSearch( 50, prevStartAt );
} );
}
if ( nextStartAt ) {
$next.on( 'click', function( evt ) {
evt.preventDefault();
doSearch( 50, nextStartAt );
} );
}
return $pagination;
}
function doSearch( fetch, startAt ) {
var curStartAt = null,
$form = $( 'form.cgroupfinder-form' ),
$resultContainer = $( '.cgroupfinder-search-result-container' ),
$input = $form.find( 'input[name="category"]' ),
$submit = $form.find( 'button[type="submit"]' ),
topCat = $input.val(),
tpl = $form.data( 'cgroup-template' );
topCat = normalizeCategoryTitle( topCat );
$input.val( topCat );
if ( startAt ) {
// do deepcopy
curStartAt = $.extend( /* deep =*/true, {}, startAt );
}
$form.data( 'cgroupfinder-loading', true );
$submit
.prop( 'disabled', true )
.text( mw.message( 'cgroupfinder-searching' ).text() )
.prepend( '<span class="mw-ajax-loader" style="top: 0;">' );
$resultContainer.find( '.cgroupfinder-pagination' ).remove();
return flattenNontransArticles( topCat, tpl, fetch, curStartAt )
.then( function( titles, nextStartAt, isEnd ) {
$resultContainer
.replaceWith(
uiSearchResult( titles, topCat, startAt, nextStartAt, isEnd )
);
$form.data( 'cgroupfinder-loading', false );
$submit
.prop( 'disabled', false )
.text( mw.message( 'cgroupfinder-search' ) );
} );
}
function uiContainer( tpl ) {
var $container = $( '<div class="cgroupfinder">' ),
$form = uiSearchForm( tpl ).appendTo( $container ),
$resultContainer = $(
'<div class="cgroupfinder-search-result-container">' )
.appendTo( $container );
$container.append( '<hr>' );
$form.submit( function( evt ) {
evt.preventDefault();
if ( !$form.data( 'cgroupfinder-loading' ) ) {
doSearch( 50, null, null );
}
} );
return $container;
}
function initFinder() {
var tpl = mw.config.get( 'wgPageName' ),
$container = uiContainer( tpl ).prependTo( '#mw-content-text' );
}
function autoInit() {
var init = true;
init = init && mw.config.get( 'wgAction' ) === 'view';
init = init && /^(Template|Module|模块):CGroup\//.test(
mw.config.get( 'wgPageName' ) );
init = init && $( '[data-cgroup-module-exists]' ).length === 0;
if ( init ) {
mw.loader.using( [
'ext.inputBox.styles',
'mediawiki.ui.input',
'mediawiki.ui.button'
], initFinder );
}
}
$.each(
wgULS( /* zh-hans =*/{
'cgroupfinder-search-label': '查找缺少$1组转换的页面:',
'cgroupfinder-input-placeholder': '输入分类名称',
'cgroupfinder-search': '搜索',
'cgroupfinder-searching': '搜索中',
'cgroupfinder-search-result-description':
'以下列出了所输入分类下(包含子分类)所有缺少当前组转换的页面。',
'cgroupfinder-pagination-prev': '上一页',
'cgroupfinder-pagination-next': '下一页'
}, /* zh-hant =*/{
'cgroupfinder-search-label': '查詢缺少$1組轉換的頁面:',
'cgroupfinder-input-placeholder': '鍵入分類名稱',
'cgroupfinder-search': '搜尋',
'cgroupfinder-searching': '搜尋中',
'cgroupfinder-search-result-description':
'以下列出了所鍵入分類下(包含子分類)所有缺少當前組轉換的頁面。',
'cgroupfinder-pagination-prev': '上一頁',
'cgroupfinder-pagination-next': '下一頁'
} ),
function( selection, value ) {
mw.messages.set( selection, value );
}
);
autoInit();