/* dplupdate.js : Script to update monthly Disambiguation Pages with Links page
* Copyright (c) 2012-22 [[en:User:R'n'B]]
* Creative Commons Attribution-ShareAlike License applies
* Requires wrappi.js, MediaWiki 1.17, and jQuery 1.7 (included with MediaWiki)
* /
// Version 0.13
/*global console, mw, jQuery, importScript, OO */
/*jshint multistr: true */
(function ($) {
var api, old_wikitext, wikitext, timestamp,
donetext = [], notdonetext = [], nolongertext = [],
continued = {},
DABCOUNT = 1000,
LOWCOUNT = 1, // mark Done if less than this many links
HIGHCOUNT = 10, // mark not Done if more than this many links
progress = 0,
pages_done = 0,
nolonger_dabs = 0,
editsummary = "Update count", // default
pagedata = {
'todo': {}, 'done': {},
'todocount': 0, 'donecount': 0,
'todolinks': 0, 'donelinks': 0
},
pattern = /^# (?:\[\[File:[^\]]*\]\])*\[\[([^\]]+)\]\]: ([0-9,]+) (\[\[Special:[^\n]+)$/gm,
pageid = mw.config.get('wgArticleId'),
fmt = function (num) {
while (num.match(/\d{4}/) !== null) {
num = num.replace(/(\d)(\d\d\d)(,|$)/, "$1,$2$3");
}
return num;
},
load_page = function () {
// retrieve the wikitext of this page
api.request({
'action': 'query',
'pageids': pageid,
'prop': 'revisions',
'rvprop': 'content|timestamp'
},
read_page
);
},
read_page = function (response) {
// create an internal representation of the page lists
var todomark, donemark, match, title, links, fmtnum;
mw.RnB.pagedata = pagedata;
mw.RnB.wikitext = wikitext = response.query.pages[pageid].revisions[0]['*'];
mw.RnB.continued = continued;
timestamp = response.query.pages[pageid].revisions[0].timestamp;
old_wikitext = wikitext;
todomark = wikitext.indexOf("===To do===");
donemark = wikitext.indexOf("===Done===");
while ((match = pattern.exec(wikitext)) !== null) {
title = match[1].trim();
links = parseInt(match[2].replace(/,/g, ''), 10);
if (match.index > todomark && match.index < donemark) {
if (pagedata.todo[title]) {
OO.ui.alert('Page "' + title + '" appears twice in the "To do" section.', {title: 'Duplicate page error'} );
} else {
pagedata.todo[title] = links;
pagedata.todocount += 1;
pagedata.todolinks += links;
}
} else if (match.index > donemark) {
if (pagedata.done[title]) {
OO.ui.alert('Page "' + title + '" appears twice in the "Done" section.', {title: 'Duplicate page error'} );
} else if (pagedata.todo[title]) {
OO.ui.alert('Page "' + title + '" appears in both the "To do" and "Done" sections.', {title: 'Duplicate page error'} );
} else {
pagedata.done[title] = links;
pagedata.donecount += 1;
pagedata.donelinks += links;
}
}
}
if (pagedata.todocount + pagedata.donecount !== DABCOUNT) {
fmtnum = fmt((pagedata.todocount + pagedata.donecount).toString());
OO.ui.alert('Parsed link counts for ' + fmtnum + ' pages.', {title: 'Page count error'} );
return;
}
},
update_totals = function () {
var progress1 = /out of a total of ([0-9,]+) links, approximately ([0-9,]+) have currently been fixed/,
progress2 = /\{\{Progress bar\|([0-9]+)\|total=([0-9]+)\|width=60%\}\}/,
m1 = progress1.exec(wikitext),
m2 = progress2.exec(wikitext),
p1, p2, num1, num2;
mw.log(pagedata.donelinks, "out of", pagedata.todolinks + pagedata.donelinks);
if (m1 === null || m2 === null) {
OO.ui.alert('Unable to parse link counts from "Progress" section of page.', {title: 'Format error'} );
return;
}
if (m2[1] === pagedata.donelinks.toString()) {
// no change, so don't update
return;
}
num1 = (pagedata.todolinks + pagedata.donelinks).toString();
num2 = pagedata.donelinks.toString();
p1 = ("out of a total of " + fmt(num1) +
" links, approximately " + fmt(num2) +
" have currently been fixed");
p2 = ("{{Progress bar|" + num2 +
"|total=" + num1 + "|width=60%}}");
wikitext = wikitext.replace(progress1, p1).replace(progress2, p2);
// ask server to generate the diff
api.request({
action: "compare",
fromid: pageid,
toslots: "main",
"totext-main": wikitext,
"tocontentformat-main": "text/x-wiki",
prop: "diff"
}, showdiff);
},
showdiff = function (response) {
var original = mw.util.$content.html(),
diff = response.compare['*'],
diffhtml = (
'<table class="diff diff-contentalign-left">\
<colgroup>\
<col class="diff-marker">\
<col class="diff-content">\
<col class="diff-marker">\
<col class="diff-content">\
</colgroup>\
<tbody>\
<tr valign="top">\
<td class="diff-otitle" colspan="2">Latest revision</td>\
<td class="diff-ntitle" colspan="2">Your text</td>\
</tr>' + diff + '</table>');
if (! diff) {
return;
}
mw.util.$content.html(diffhtml);
OO.ui.prompt('Accept these changes?', {textInput: {value: editsummary}}).done(
function (result) {
if (result !== null) {
// save the page
api.request({
action: 'edit',
title: mw.config.get("wgPageName"),
text: wikitext,
summary: result,
token: mw.user.tokens.get("csrfToken"),
basetimestamp: timestamp
}, page_saved);
} else {
mw.util.$content.html(original);
wikitext = old_wikitext;
}
}
);
},
page_saved = function (response) {
// load edited page
if (response.edit.result === "Success") {
window.location.reload();
return;
}
OO.ui.alert("Error saving page: '" + response.edit.toString() + "'", {title: "API error"} );
mw.log(response);
},
minmax = function (min, val, max) {
if (val < min) return min;
if (val > max) return max;
return val;
},
lines = 2.5,
progressDialog,
update_pages = function () {
// dispatch backlinks requests for all linked pages
var titlelist = [],
t;
// this is going to take a while, so set up progress dialog
function ProgressDialog( config ) {
ProgressDialog.super.call( this, config );
}
OO.inheritClass( ProgressDialog, OO.ui.Dialog );
ProgressDialog.static.name = 'progressDialog';
ProgressDialog.prototype.initialize = function () {
ProgressDialog.super.prototype.initialize.call( this );
this.content = new OO.ui.PanelLayout( { padded: true, expanded: true, scrollable: true } );
this.content.$element.append( '<p>Now updating backlink counts to disambig pages. Press Escape key to cancel.</p>');
this.progbar = new OO.ui.ProgressBarWidget({ id: 'dplprogress', progress: 0 });
this.progbar.toggle(true);
this.content.$element.append(this.progbar.$element);
this.content.$element.append($('<div id="dpllog"></div>'));
this.$body.append( this.content.$element );
};
ProgressDialog.prototype.getBodyHeight = function () {
return this.content.$element.outerHeight( true ) * minmax(3, lines, 12);
};
progressDialog = new ProgressDialog( {size: 'larger'} );
// Create and append a window manager, which opens and closes the window.
var windowManager = new OO.ui.WindowManager();
$( document.body ).append( windowManager.$element );
windowManager.addWindows( [ progressDialog ] );
windowManager.openWindow( progressDialog );
for (t in pagedata.todo) {
if (pagedata.todo.hasOwnProperty(t)) {
titlelist.push(t);
}
}
for (t in pagedata.done) {
if (pagedata.done.hasOwnProperty(t)) {
titlelist.push(t);
}
}
while (titlelist.length) {
t = titlelist.shift();
// retrieve backlinks, also categories and page info to
// confirm whether this is a redirect and/or no longer a dab
api.request({
action: 'query',
prop: 'info|categories',
titles: t,
indexpageids: "",
redirects: "",
list: 'backlinks',
bltitle: t,
blnamespace: 0,
blredirect: "",
bllimit: "max"
}, process_count);
}
},
process_count = function (response, query) {
// count the eligible backlinks from the API response
var item, redir, len, redirlen, ititle, rd, donemark,
page = response.query.pages[response.query.pageids[0]],
bl = response.query.backlinks,
count = 0,
dabtitle = query.bltitle,
isredirect = false,
hasdabcat = false,
conceptdab = false,
p = new RegExp('# ((?:\\[\\[File:[^\\]]*\\]\\])*)\\[\\[' +
mw.util.escapeRegExp(dabtitle) +
'\\]\\]: ([0-9,]+) (\\[\\[Special:[^\\n]+)\\n'),
m = p.exec(wikitext);
if (! continued[dabtitle]) {
continued[dabtitle] = {};
}
// first check that this is still a dab
if (response.query.redirects) {
isredirect = true;
}
if (page.categories) {
len = page.categories.length;
for (item = 0; item < len; item += 1) {
if (page.categories[item].title ===
"Category:All article disambiguation pages") {
hasdabcat = true;
}
if (page.categories[item].title ===
"Category:Disambiguation pages to be converted to broad concept articles") {
conceptdab = true;
}
}
}
if (! hasdabcat) {
// no need to count backlinks if no longer a disambig page
if (pagedata.todo[dabtitle]) {
// move to done section
wikitext = wikitext.replace(p, '').concat(
'\n# ' + m[1] + '[[' + dabtitle + ']]: ' + m[2] + " " + m[3] +
" - no longer a disambig page");
if (dabtitle === "State") { mw.log(wikitext); }
pagedata.done[dabtitle] = pagedata.todo[dabtitle];
pagedata.todocount -= 1;
pagedata.donecount += 1;
pagedata.todolinks -= pagedata.todo[dabtitle];
pagedata.donelinks += pagedata.todo[dabtitle];
delete pagedata.todo[dabtitle];
$("<p></p>").text(
"[[" + dabtitle + "]] is no longer a dab, moved to 'done'")
.appendTo($('#dpllog'));
lines += 0.9;
progressDialog.updateSize();
nolongertext.push("[[" + dabtitle + "]]");
nolonger_dabs += 1;
}
progress += 1;
progressDialog.progbar.setProgress(progress * 100 / DABCOUNT);
mw.log("Progress = " + progress);
if (progress === DABCOUNT) {
check_done();
}
return;
}
// save results
len = bl.length;
for (item = 0; item < len; item += 1) {
ititle = bl[item].title;
if (bl[item].redirlinks) {
rd = bl[item].redirlinks;
redirlen = rd.length;
if (! continued[dabtitle][ititle]) {
continued[dabtitle][ititle] = {};
}
for (redir = 0; redir < redirlen; redir += 1) {
continued[dabtitle][ititle][rd[redir].title] = true;
}
} else {
if (! bl[item].hasOwnProperty('redirect')) {
continued[dabtitle][ititle] = true;
}
}
}
if (response['query-continue'] && response['query-continue'].blcontinue) {
// continue query
query.blcontinue = response['query-continue'].blcontinue;
query.rawcontinue = "";
api.request(query, process_count);
return;
}
if (conceptdab) {
// mark, if not already marked
if (m[3].indexOf("DABCONCEPT") === -1) {
wikitext = wikitext.replace(p, $.trim(m[0].slice(0, -1)) +
" – tagged as [[WP:DABCONCEPT]]\n");
}
}
// count the eligible links
bl = continued[dabtitle];
for (item in bl) {
if (bl.hasOwnProperty(item)) {
if (bl[item] === true) {
if (item !== dabtitle && !dabtitle.endswith("(disambiguation)")) {
count += 1;
}
} else {
// item is a redirect with its own backlinks
if (! item.endswith("(disambiguation)")) {
for (rd in bl[item]) {
if (bl[item].hasOwnProperty(rd)) {
count += 1;
}
}
}
}
}
}
// see if dabtitle needs to be moved to the other section
if (pagedata.todo[dabtitle] && count < LOWCOUNT) {
// this is done
wikitext = wikitext.replace(p, '').concat(
'\n# ' + m[1] + '[[' + dabtitle + ']]: ' + m[2] + " " + m[3]);
pagedata.done[dabtitle] = pagedata.todo[dabtitle];
pagedata.todocount -= 1;
pagedata.donecount += 1;
pagedata.todolinks -= pagedata.todo[dabtitle];
pagedata.donelinks += pagedata.todo[dabtitle];
delete pagedata.todo[dabtitle];
$("<p></p>").text(
"[[" + dabtitle + "]] (" + count + " links) moved to 'done'")
.appendTo($('#dpllog'));
lines += 0.9;
progressDialog.updateSize();
donetext.push("[[" + dabtitle + "]]");
pages_done += 1;
} else if (pagedata.done[dabtitle] && count > HIGHCOUNT) {
// this is un-done
wikitext = wikitext.replace(p, '');
donemark = wikitext.indexOf("===Done===");
wikitext = wikitext.slice(0, donemark-1) +
('# ' + m[1] + '[[' + dabtitle + ']]: ' + m[2] + " " + m[3] + '\n') +
wikitext.slice(donemark-1);
pagedata.todo[dabtitle] = pagedata.done[dabtitle];
pagedata.donecount -= 1;
pagedata.todocount += 1;
pagedata.donelinks -= pagedata.done[dabtitle];
pagedata.todolinks += pagedata.done[dabtitle];
delete pagedata.done[dabtitle];
mw.log("Info:", dabtitle, "moved back to 'to do'");
$("<p></p>").text(
"[[" + dabtitle + "]] (" + count + " links) moved back to 'To do'")
.appendTo($('#dpllog'));
lines += 0.9;
progressDialog.updateSize();
notdonetext.push("[[" + dabtitle + "]]");
}
if (pagedata.todo[dabtitle] && isredirect) {
$("<p></p>").text(
"[[" + dabtitle + "]] is now a redirect to [[" +
response.query.redirects[0].to + "]]"
).appendTo($('#dpllog'));
lines += 0.9;
}
progress += 1;
progressDialog.progbar.setProgress(progress * 100 / DABCOUNT);
mw.log("Progress = " + progress);
if (progress === DABCOUNT) {
progressDialog.close();
check_done();
}
},
check_done = function () {
// this should be called by the progress bar when it hits 100%
var es1 = '', es2 = '', es3 = '',
jointhem = function (s1, s2) {
return (s1 && s2) ? (s1 + "; " + s2) : (s1 || s2);
};
mw.log("check_done called");
$('#progressdiv').dialog('destroy');
editsummary = "";
if (donetext.length > 0) {
es1 = donetext.join(", ") + " done";
}
if (nolongertext.length > 0) {
es2 = nolongertext.join(", ") + " no longer " +
((nolongertext.length > 1) ? "dabs" : "a dab");
}
if (notdonetext.length > 0) {
es3 = notdonetext.join(", ") + " still " +
((notdonetext.length > 1) ? "have" : "has") + " links to fix";
}
// create tentative editsummary
editsummary = jointhem(jointhem(es1, es2), es3);
if (editsummary.length > 480 && nolonger_dabs > 0) {
es2 = nolonger_dabs.toString() +
" no longer " + ((nolonger_dabs > 1) ? "dabs" : "a dab");
editsummary = jointhem(jointhem(es1, es2), es3);
}
if (editsummary.length > 480 && pages_done > 0) {
es1 = pages_done.toString() + " page" +
((pages_done > 1) ? "s" : "") + " done";
editsummary = jointhem(jointhem(es1, es2), es3);
}
if (editsummary === "") {
editsummary = "Update count";
} else {
editsummary += "; update count";
}
update_totals();
};
String.prototype.endswith = function (substring) {
// return true if substring matches the end of this
return (this.slice(-substring.length) === substring);
};
$(function () { // on document ready:
var startup = function () {
if (mw.RnB && mw.RnB.Wiki) {
mw.loader.using([
'oojs-ui-core',
'oojs-ui-windows',
'mediawiki.diff.styles'
],
function () {
mw.util.addPortletLink("p-tb", "#",
"Update totals only", "tb-totals");
mw.util.addPortletLink("p-tb", "#",
"Update pages and totals", "tb-update");
$('#tb-totals').click(function (e) {
update_totals();
e.preventDefault();
});
$('#tb-update').click(function (e) {
update_pages();
e.preventDefault();
});
api = new mw.RnB.Wiki();
load_page();
});
} else {
setTimeout(startup, 100);
}
};
startup();
});
}(jQuery) );