User:R'n'B/birthdeath.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.
/* birthdeath.js : Wikipedia app to add birth and/or death dates to
 *  a human name disambiguation page
 * Copyright (c) 2011 [[en:User:R'n'B]]
 *  Creative Commons Attribution-ShareAlike License applies
 * Requires wrappi.js, MediaWiki 1.18, and jQuery 1.6 (included with MediaWiki)
 * <nowiki>
 */
// Version 0.1; alpha release
/*global console, mw, jQuery, importScript, importStylesheet */
(function ($) {
	// pseudo-global variables
	var api,
		app,
		onready,
		original,
		startup,
		linkRx = /\[\[([^\]\|\[#<>\{\}]*)(\|.*?)?\]\]/, // groups: title, anchor
		bornRx = / *\( *[Bb](?:orn|\.) +(\d+(?: *BC)?) *\) */,
		diedRx = / *\( *[Dd](?:ied|\.) +(\d+(?: *BC)?) *\) */,
		datesRx = / *\( *(\d+(?: *BC)?) *(?:[\-\u2013\u2014]|&ndash;|\{\{ *ndash *\}\}) *(\d+(?: *BC)?) *\) */,
		birthcats = /Category:(\d{1,4}(?: BC)?) births/,
		deathcats = /Category:(\d{1,4}(?: BC)?) deaths/;
	importScript('User:Cacycle/diff.js');
	importScript("User:R'n'B/wrappi.js");
	importStylesheet("User:R'n'B/birthdeath.css");
	onready = function () { // page setup that doesn't require API access
		var cpointer = function () { $(this).css("cursor", "pointer"); },
			cdefault = function () { $(this).css("cursor", "default"); };
		original = {
			'#content': $('#content').html()
		};
		$('#content').empty().addClass("bd-content")
		.append(
			$('<div class="bd-banner">Human name disambiguation page fixer</div>')
				.append($('<div style="float: right; margin: 0px 0.25em"></div>')
					.append($('<img alt="Close" id="bd-close"' +
'src="http://bits.wikimedia.org/skins-1.17/common/images/closewindow.png">'))
				)
		).append(
			$('<h1 id="firstHeading" class="firstHeading"></h1>')
				.text('(Loading...)')
		).append(
			$('<div id="bodyContent"></div>').html(
'<div id="wikEdDiffWrapper" class="wikEdDiffWrapper">'+
'<div id="wikEdDiffDiv" class="wikEdDiffDiv"></div>'+
'<div id="wikEdDiffButtonWrapper" class="wikEdDiffButtonWrapper">'+
'<button id="wikEdDiffButton" title="Refresh diff view" class="wikEdDiffButton">'+
'<img id="wikEdDiffButtonImg" src="http://upload.wikimedia.org/wikipedia/commons/c/c6/WikEdDiff.png" title="Refresh diff view" alt="wikEdDiff">'+
'</button></div>'+
'</div><!-- wikEdDiffWrapper -->' +
'<form id="editform">' +
'<div class="wikiEditor-ui-text">' +
'<textarea style="" rows="25" cols="80" id="wpTextbox1" accesskey="," tabindex="1">' +
'</textarea>' +
'</div>' +
'<div style="clear: both;"></div>' +
'<div id="editpage-copywarn">' +
'<p>Content that violates any copyrights will be deleted. Encyclopedic content ' +
'must be <b><a title="Wikipedia:Verifiability"' +
' href="/wiki/Wikipedia:Verifiability">verifiable</a></b>.</p>' +
'<p>By clicking the "Save Page" button, you agree to the ' +
'<a title="wmf:Terms of Use" class="extiw" href="http://wikimediafoundation.org/wiki/Terms_of_Use">' +
'Terms of Use</a>, and you irrevocably agree to release your contribution under ' +
'the <a title="Wikipedia:Text of Creative Commons Attribution-ShareAlike 3.0 Unported License"' +
' href="/wiki/Wikipedia:Text_of_Creative_Commons_Attribution-ShareAlike_3.0_Unported_License">' +
'CC-BY-SA 3.0 License</a> and the ' +
'<a title="Wikipedia:Text of the GNU Free Documentation License"' +
' href="/wiki/Wikipedia:Text_of_the_GNU_Free_Documentation_License">GFDL</a>. ' +
'You agree that a hyperlink or URL is sufficient attribution under the Creative Commons license.</p>' +
'</div><!-- editpage-copywarn -->' +
'<div class="editOptions">' +
'<span id="wpSummaryLabel" class="mw-summary">' +
'<label for="wpSummary"><span style="text-align: left;">' + 
'<a title="Help:Edit summary" href="/wiki/Help:Edit_summary">Edit summary</a> ' +
'<small>(Briefly describe the changes you have made)</small></span></label>' +
'</span> ' +
'<input type="text" name="wpSummary" value="" accesskey="b" title="Enter a short summary [alt-shift-b]" size="60" tabindex="1" maxlength="250" id="wpSummary" class="mw-summary">'+
'<div class="editCheckboxes">' +
'<input type="checkbox" id="wpMinoredit" accesskey="i" tabindex="3" value="1" name="wpMinoredit">&nbsp;' +
'<label title="Mark this as a minor edit [alt-shift-i]" id="mw-editpage-minoredit" for="wpMinoredit">This is a minor edit '+
'<span id="minoredit_helplink">(<a title="Help:Minor edit" href="/wiki/Help:Minor_edit">what\'s this?</a>)</span>' +
'</label>' +
'<input type="checkbox" id="wpWatchthis" accesskey="w" tabindex="4" checked="checked" value="1" name="wpWatchthis">&nbsp;' +
'<label title="Add this page to your watchlist [alt-shift-w]" id="mw-editpage-watch" for="wpWatchthis">Watch this page</label>' +
'</div><!-- editCheckboxes -->' +
'<div class="editButtons">' +
'<input type="button" title="Save your changes [alt-shift-s]" accesskey="s" value="Save page" tabindex="5" name="wpSave" id="wpSave">' +
'<input type="button" title="Preview your changes; please use this before saving. [alt-shift-p]" accesskey="p" value="Show preview" tabindex="6" name="wpPreview" id="wpPreview">' +
'<input type="button" title="Show which changes you made to the text [alt-shift-v]" accesskey="v" value="Show changes" tabindex="7" name="wpDiff" id="wpDiff">' +
'<span class="editHelp"><a id="mw-editform-exit" title="Exit this application" >Cancel</a>&nbsp;| ' +
'<a id="mw-editform-skip" title="Go to next unchecked page">Skip</a>&nbsp;| ' +
'<a href="/wiki/Wikipedia:Cheatsheet" target="helpwindow">Editing help</a> (opens in new window)</span>' +
'</div><!-- editButtons -->' +
'</div><!-- editOptions -->' +
'<div class="mw-tos-summary">' +
'<p><small id="mw-wikimedia-editpage-tos-summary">If you do not want your ' +
'writing to be edited, used, and redistributed at will, then do not submit it ' +
'here. All text that you did not write yourself, except brief excerpts, must ' +
'be available under terms consistent with Wikipedia\'s ' +
'<b><a title="foundation:Terms of Use" class="extiw" href="http://wikimediafoundation.org/wiki/Terms_of_Use">Terms of Use</a></b> '+
'before you submit it.</small>' +
'</p></div><!-- mw-tos-summary --></form><!-- editform -->' +
'<div id="dialog-form" title="Human name disambiguation page fixer">' +
'<form>' +
'<fieldset>' +
'<label for="startfrom">Start from</label>' +
'<input type="text" name="startfrom" id="startfrom" class="text ui-widget-content ui-corner-all" value="' + app.startfrom + '" /><br />' +
'<label for="skipunchanged">Skip pages without suggested changes</label>' +
'<input type="checkbox" name="skipunchanged" id="skipunchanged" value="" class="ui-widget-content ui-corner-all" />' +
'</fieldset>' +
'</form>' +
'</div><!-- dialog-form -->'
			)
		);
		// assign actions to form elements
		$('#bd-close').click(app.exit).hover(cpointer, cdefault);
		$('#wikEdDiffButton').click(app.showdiff);
		$('#wpSave').click(app.savepage);
		$('#wpPreview').attr("disabled", true);
		$('#wpDiff').click(app.showdiff);
		$('#mw-editform-exit').click(app.exit).hover(cpointer, cdefault);
		$('#mw-editform-skip').click(app.loadnext).hover(cpointer, cdefault);
	};
	app = mw.RnB.birthdeath = {
		startfrom: mw.cookie.get('birthdeathstart') || '!',
		setup: function () { // initialization that does require API access
			api = new mw.RnB.Wiki();
			$('#dialog-form').dialog({
				autoOpen: true,
				modal: true,
				buttons: {
					'Go': function () {
						app.startfrom = $('#startfrom').val();
						app.skipunchanged = $('#skipunchanged:checked').length;
							// will be 1 if checked, 0 if not
						$(this).dialog("close");
						app.go();
					},
					'Cancel': function() {
						$(this).dialog("close");
						app.exit();
					}
				}
			});
		},
		go: function() {
			var copy = {};
			$('#dialog-form').dialog("close");
			// retrieve list of hndis pages
			app.cmquery = {
				action: 'query',
				rawcontinue: '',
				generator: 'categorymembers',
				gcmtitle: 'Category:Human name disambiguation pages',
				gcmnamespace: '0',
				gcmstartsortkeyprefix: app.startfrom,
				gcmlimit: '20',
				indexpageids: true,
				prop: 'categories|info|revisions',
				clcategories: "Category:Human name disambiguation pages",
				cllimit: 'max',
				clprop: 'sortkey',
				inprop: 'watched',
				intoken: 'edit',
				rvprop: 'content|timestamp'
			};
			app.catmemlist = [];
			$('body').css("cursor", "wait");
			$.extend(copy, app.cmquery);
			api.request(copy, app.listrecvd);
		},
		listrecvd: function (response, query) {
			// received list of category members from server
			var pagelist = [],
				rq = response.query,
				rqc = response["query-continue"];
			app.cmquery = $.extend({}, query);
			if (rqc && rqc.categorymembers) {
				app.cmquery.gcmcontinue = rqc.categorymembers.gcmcontinue;
			} else {
				delete app.cmquery.gcmcontinue;
			}
			if (rq && rq.pages) {
				if (rq.pageids) {
					// server provides index for sorting the results
					$.each(rq.pageids, function () {
						var item = rq.pages[this];
						if (item.categories) {
							pagelist.push({
								sortkey: item.categories[0].sortkeyprefix, 
								text: item.revisions[0]['*'],
								timestamp: item.revisions[0].timestamp,
								title: item.title, 
								token: item.edittoken,
								watched: (item.watched !== undefined)
							});
						}
					});
				} else {
					// we have to sort ourselves, using the sortkey
					$.each(rq.pages, function () {
						if (this.categories) {
							pagelist.push({
								sortkey: this.categories[0].sortkeyprefix, 
								text: this.revisions[0]['*'],
								timestamp: this.revisions[0].timestamp,
								title: this.title, 
								token: this.edittoken,
								watched: (this.watched !== undefined)
							});
						}
					});
					pagelist.sort(function (a, b) {
						var as = a.sortkey,
							bs = b.sortkey;
						return as < bs ? -1 : as > bs ? 1 : 0;
					});
				}
			}
			$.merge(app.catmemlist, pagelist);
			if (! query.gcmcontinue) {
				// this is the first chunk received, not a continuation
				app.loadnext();
			}
		},
		loadnext: function () {
			// load next disambig page and start processing
			var q,
				page = app.currentpage = app.catmemlist.shift();
			app.currentlinks = {};
			app.currentredirs = {};
			if (! page) {
				app.exit();
				return;
			}
			mw.cookie.set("birthdeathstart", page.sortkey, {expires: 3652});
			$('#firstHeading').text(page.title);
			// load the pages linked from this disambiguation page
			api.request(
				{	action: 'query',
					generator: 'links',
					rawcontinue: '',
					titles: page.title,
					redirects: true,
					gplnamespace: '0',
					gpllimit: 'max',
					prop: 'info|revisions|categories',
					rvprop: 'content',
					cllimit: 'max'
				}, app.processlinks
			);
			$('#wpTextbox1').hide().val(app.currentpage.text);
			$('#wikEdDiffDiv').hide();
			$('#wpMinoredit').attr("checked", "checked");
			if (page.watched || mw.user.options.get("watchdefault")) {
				$('#wpWatchthis').attr("checked", "checked");
			} else {
				$('#wpWatchthis').prop("checked", false);
			}
			$('input[name="wpSummary"]').val(
				"Add birth/death dates to hndis entries, from linked article(s)");
			$('body').css("cursor", "default");
			// continue query in background if list is almost empty
			if (app.catmemlist.length < 5 && app.cmquery.gcmcontinue) {
				q = $.extend({}, app.cmquery);
				api.request(q, app.listrecvd);
				delete app.cmquery.gcmcontinue;
			}
		},
		processlinks: function (response, query) {
			// go through blue links on page and propose changes to text
			var rq = response.query,
				rqc = response["query-continue"],
				lines = app.currentpage.text.split("\n"),
				links = {},
				newtext,
				redirs = {},
				len = lines.length,
				i;
			if (rqc && rqc.links) {
				// unlikely any hndis page would contain more than 500 links,
				// but just in case...
				query.gplcontinue = rqc.links.gplcontinue;
				api.request(query, app.processlinks);
			}
			if (rq && rq.redirects) {
				// create a map from redirect sources to target pages
				$.each(rq.redirects, function () {
					redirs[this.from] = this.to;
				});
				$.extend(app.currentredirs, redirs);
			}
			if (rq && rq.pages) {
				// index links by linked page title
				$.each(rq.pages, function () {
					links[this.title] = this;
				});
				$.extend(app.currentlinks, links);
				// go through each line in disambig page, see if it starts with
				// a blue link
				$.each(lines, function (index) {
					var thislink,
						born = null,
						born_already = null,
						died = null,
						died_already = null,
						m = this.match(linkRx),
						n,
						revised = this.slice(0),
						bold,
						newtext; // copy of line
					if (m !== null && m.length > 1) {
						// link found; find corresponding object in links
						thislink = redirs[m[1]]
							? links[redirs[m[1]]]
							: links[m[1]];
						if (thislink === undefined) {
							// invalid link, probably to a category or iw
							return;
						}
						if (thislink.missing !== undefined) {
							// this is a red link, skip it
							return;
						}
						// blue link found; try to find birth/death year
						// in categories
						$.each(thislink.categories, function () {
							var m1 = birthcats.exec(this.title),
								m2 = deathcats.exec(this.title);
							if (m1 !== null) {
								born = m1[1];
							}
							if (m2 !== null) {
								died = m2[1];
							}
						});
						// here we could, if desired, look for birth/death
						// years in other places, like {{Persondata}}
						// ...
						if (born === null && died === null) {
							return;
						}
						// birth and/or death year found;
						// make sure the current line doesn't already
						// contain them
						n = bornRx.exec(this);
						if (n !== null) {
							born_already = n[1];
						}
						n = diedRx.exec(this);
						if (n !== null) {
							died_already = n[1];
						}
						n = datesRx.exec(this);
						if (n !== null) {
							born_already = n[1];
							died_already = n[2];
						}
						if (born && !born_already) {
							if (died && !died_already) {
								// supply both birth and death years
								newtext = " (" + born + "–" + died + ")";
							} else {
								// supply birth year only
								if (died_already) {
									newtext = " (" + born + "–" + died_already + ")";
								} else {
									newtext = " (born " + born + ")";
								}
							}
						} else if (died && !died_already) {
							if (born_already) {
								newtext = " (" + born_already + "–" + died + ")";
							} else {
								newtext = " (died " + died + ")";
							}
						}
						if (newtext) {
							bold = "'''" + m[0] + "'''";
							if (revised.indexOf(bold) !== -1) {
								revised = revised.replace(bold, bold + newtext);
							} else {
								revised = revised.replace(m[0], m[0] + newtext);
							}
						}
					}
					lines[index] = revised;
				});
			}
			newtext = lines.join('\n');
			if (app.skipunchanged && newtext == app.currentpage.text) {
				return app.loadnext();
			}
			//cosmetic changes
			newtext = newtext
				.replace(/&ndash;/g, '–')
				.replace(/\{\{ *ndash *\}\}/g, '–')
				.replace(/\( *b\. */i, '(born ')
				.replace(/\( *d\. */i, '(died ')
				.replace(/\{\{ *[Rr]efer *(\|.*?)?\}\}/, '{{subst:refer$1}}')
				.replace(/\n*(\{\{[Hh]ndis.*?\}\})\n*/, '\n\n$1\n\n')
				.replace(/\n*$/, '\n');
			$('#firstHeading').append(
				$('<span></span>').text("Skip").attr("id", "topskip")
					.css("font-size", "50%")
			);
			$('#topskip').button().click(app.loadnext);
			$('#wpTextbox1').val(newtext).show();
			app.showdiff();
		},
		showdiff: function () {
			// refresh the WikEdDiff display
			var m,
				link,
				linkedpage,
				text = app.currentpage.text,
				diff = window.WDiffString(text, $('#wpTextbox1').val()),
				allLinkRx = /\[\[([^\]\|\[#<>\{\}]*)(\|.*?)?\]\]([a-z]*)/g;
					// same as linkRx, but with /g flag
			// find each link in the text, and if it matches an article link
			// from the database, wrap it in an <a> or <span> element
			while ((m = allLinkRx.exec(text)) !== null) {
				// linked title = m[1]
				// anchor = m[2]
				// trail = m[3]
				linkedpage = app.currentredirs[m[1]]
							? app.currentlinks[app.currentredirs[m[1]]]
							: app.currentlinks[m[1]];
				if (linkedpage !== undefined && linkedpage.missing !== undefined) {
					link = $('<span></span>')
						.addClass("new")
						.attr("title", (m[2] ? m[2].replace(/^\|/, '') : m[1])
							+ (m[3] || '') + ' (page does not exist)')
						// FIXME above is wrong, doesn't account for pipe tricks!
						.append($('<span></span>').css("color", "black").text(m[0]));
				} else {
					link = $('<a></a>')
						.addClass("link")
						.attr("href", 
							mw.config.get('wgArticlePath')
								.replace('$1',
									mw.util.wikiUrlencode(m[1]).replace(/ /g, '_')
								)
						).attr("target", "_blank")
						.attr("title", (m[2] ? m[2].replace(/^\|/, '') : m[1])
							+ (m[3] || ''))
						// FIXME above is wrong, doesn't account for pipe tricks!
						.append($('<span></span>').css("color", "black").text(m[0]));
					if (app.currentredirs[m[1]]) {
						link.addClass("mw-redirect")
							.attr("title",
								link.attr("title") + ' (redirect to [[' 
								+ app.currentredirs[m[1]] + ']])');
					}
				}
				// FIXME doesn't work if there are 2 identical links
				diff = diff.replace(
					m[0].replace(/"/g, '&quot;'),
					link.wrap("<span>").parent().html()
				);
			}
			$('#wikEdDiffDiv').html(diff).show();
		},
		savepage: function () {
			var t = app.currentpage.title,
				params = {
					action: 'edit',
					title: t,
					text: $('#wpTextbox1').val(),
					token: app.currentpage.token,
					basetimestamp: app.currentpage.timestamp,
					summary: $('input[name="wpSummary"]').val()
				};
			if ($('#wpMinoredit:checked').length) {
				params.minor = "";
			}
			if ($('#wpWatchthis:checked').length) {
				params.watchlist = "watch";
			}
			api.request(params,
				function () {mw.log("Page [[" + t + "]] saved");}
			);
			app.loadnext();
		},
		exit: function () {
			// Restore original contents of page
			$.each(original, function(elem, content) {
				$(elem).html(content);
			});
			$('#content').removeClass("bd-content");
		}
	};
	mw.loader.using(
		['jquery.ui', 'mediawiki.cookie',
		 'jquery.ui',
		 'jquery.ui'],
		function () {
			var startup = function () {
				if (mw.RnB && mw.RnB.Wiki) {
					$(document).ready(app.setup);
				} else {
					setTimeout(startup, 100);
				}
			};
			$(document).ready(onready);
			startup();
		}
	);
} (jQuery));
// </nowiki>