User:R'n'B/dplupdate.js

From Wikipedia, the free encyclopedia
Note: After saving, you have to bypass your browser's cache to see the changes. Google Chrome, Firefox, Microsoft Edge and Safari: Hold down the ⇧ Shift key and click the Reload toolbar button. For details and instructions about other browsers, see Wikipedia:Bypass your cache.
/* 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) );