MediaWiki:Gadget-metadata.js

/** * Metadata assessment script * Finds the WP 1.0/WikiProject assessment of every article you go to, then * displays that information in the article header. * @author Outriggr - created the script and used to maintain it * @author Pyrospirit - currently maintains and updates the script */

if (typeof(unsafeWindow) != 'undefined') {	addOnloadHook = unsafeWindow.addOnloadHook; console = unsafeWindow.console; importStylesheet = unsafeWindow.importStylesheet; mw = unsafeWindow.mw; sajax_init_object = unsafeWindow.sajax_init_object; }

// Import stylesheet with custom classes for header colors importStylesheet('User:Pyrospirit/metadata.css');

var assessment = { showOldPeerReviews: false, /**    * Starts the script object running. The main function of the script. If the * getMainType function can find the assessment, it uses that assessment * for the page, parses it, and displays it in the header. Otherwise, it runs * ajaxMain. */   init: function init  { if( !$('#siteSub').length ) return; //This skin is not compatible with the tool this.callHooks('init_before'); var initialAssessment = this.checkArticle; // checks for types visible from article page if (initialAssessment.exists) { this.currentAssessment = initialAssessment; var data = this.talkAssess(this.currentAssessment); this.update(data.newClass, data.slogan, data.info); }       else this.ajaxMain; // proceed to check the talk page this.callHooks('init_after'); },   /**     * The main function when an AJAX request is needed to find the assessment. * Creates an AJAX request for the contents of a URL (defaults to the    * first section of the article's talk page), then sends the request. After * getting the requested data back, it finds the assessment information in    * the data, then uses and displays that assessment in the header. * @param {String} url - Optional: override the default URL for the request. * @param {Function} stateChange - Optional: override the default request callback * @param optionalArgument - Optional: passed to the stateChange function */   ajaxMain: function ajaxMain (url, stateChange, optionalArgument) { if (!url || !/^https?:\/\//i.test(url)) // optional url override url = mw.config.get('wgServer') + mw.config.get('wgScript') + '?title=Talk:' + encodeURIComponent(mw.config.get('wgPageName')) + '&action=raw&section=0'; if (typeof stateChange !== 'function') { stateChange = this.stateChange; this.url = url; }       var request = sajax_init_object; if (request) { var that = this; // store value of 'this' request.onreadystatechange = function { stateChange.call(that, request, optionalArgument); }           request.open('GET', url, true); request.send(null); }   },    /**     * This function is passed as a parameter to ajaxMain. It is called each time * this.request updates, and the code inside the conditional runs when the * data is available. */   stateChange: function stateChange (request) { if (request.readyState == 4 && request.status == 200) { this.text = request.responseText; this.request = request; var rating = this.getRating(this.text); this.currentAssessment = this.getAssessment(this.text, rating); var data = this.talkAssess(this.currentAssessment); this.update(data.newClass, data.slogan, data.info); this.callHooks('onCompletedRequest'); }   },    /**     * Checks for various objects on the article page that indicate a certain * assessment, such as a featured star or disambiguation page notice. If this * function can find the assessment, AJAX is not needed for this page. * @return {Object} assess - the assessment in an easily readable format * @static */   checkArticle: function checkArticle  { var assess = {}; assess.extra == ''; assess.exists = true; if ($('#disambig').length || $('#disambig_disambigbox').length               || $('#disambigbox').length) assess.rating = 'dab'; else if ($('#setindexbox').length) assess.rating = 'setindex'; else if ($('#contentSub').length               && $('#contentSub').text == 'Redirect page') assess.rating = 'redir'; else if ($('#ca-talk').length               && $('#ca-talk').hasClass('new')) // no talk page assess.rating = 'none'; else assess.exists = false; // none of the above, no assessment found return assess; },   /**     * Searches the provided wikicode for the rating part of an assessment and * returns it as a string. * Note that a higher assessment takes priority, and less-used assessments * such as "list", "current", or "future" are used only if nothing else can * be found. * @param {String} text - some wikitext to be searched for assessment info * @return {String} rating - the article's current assessment */   getRating: function getRating (text) { this.callHooks('getRating_before'); var rating = 'none'; if (text.match(/\|\s*(class|currentstatus)\s*=\s*fa\b/i)) rating = 'fa'; else if (text.match(/\|\s*(class|currentstatus)\s*=\s*fl\b/i)) rating = 'fl'; else if (text.match(/\|\s*class\s*=\s*a\b/i)) { if (text.match(/\|\s*class\s*=\s*ga\b|\|\s*currentstatus\s*=\s*(ffa\/)?ga\b/i)) rating = 'a/ga'; // A-class articles that are also GA's           else rating = 'a'; } else if (text.match(/\|\s*class\s*=\s*ga\b|\|\s*currentstatus\s*=\s*(ffa\/)?ga\b|\{\{\s*ga\s*\|/i)                  && !text.match(/\|\s*currentstatus\s*=\s*dga\b/i)) rating = 'ga'; else if (text.match(/\|\s*class\s*=\s*b\b/i)) rating = 'b'; else if (text.match(/\|\s*class\s*=\s*bplus\b/i)) rating = 'bplus'; // used by WP Math else if (text.match(/\|\s*class\s*=\s*c\b/i)) rating = 'c'; else if (text.match(/\|\s*class\s*=\s*start/i)) rating = 'start'; else if (text.match(/\|\s*class\s*=\s*stub/i)) rating = 'stub'; else if (text.match(/\|\s*class\s*=\s*list/i)) rating = 'list'; else if (text.match(/\|\s*class\s*=\s*sl/i)) rating = 'sl'; // used by WP Plants else if (text.match(/\|\s*class\s*=\s*(dab|disambig)/i)) rating = 'dab'; else if (text.match(/\|\s*class\s*=\s*cur(rent)?/i)) rating = 'cur'; else if (text.match(/\|\s*class\s*=\s*future/i)) rating = 'future'; this.callHooks('getRating_after'); return rating; },   /**     * Searches the provided wikicode for data on the article's current and past * featured or good status and returns an object that contains this data * along with some miscellaneous other bits of information. * @param {String} text - some wikitext to be searched for assessment info * @return {Object} assess - the assessment data for the page */   getAssessment: function getAssessment (text, rating) { this.callHooks('getAssessment_before'); var assess = {rating: rating, pageLink: [null, null], extra: [], activeReview: null}; var actionNumber = 0, pageLinkFlag = false, tempMatch, articleName;

// Current nominations (FAC, FLC, or GAN) if ((assess.reg = text.match(/\{\{\s*featured[ _]article[ _]candidates\s*(?:[\|\}]\s*([^\|\}]*))?[^\}]*?\}\}/i))) { assess.extra.push('fac'); if (assess.reg[1] && (articleName = this.decodeEntities($.trim(assess.reg[1])))) assess.pageLink[0] = 'Wikipedia:Featured_article_candidates\/' + articleName; } else if ((assess.reg = text.match(/\{\{\s*featured[ _]list[ _]candidates\s*(?:[\|\}]\s*([^\|\}]*))?[^\}]*?\}\}/i))) { assess.extra.push('flc'); if (assess.reg[1] && (articleName = this.decodeEntities($.trim(assess.reg[1])))) assess.pageLink[0] = 'Wikipedia:Featured_list_candidates\/' + articleName; } else if ((assess.reg = text.match(/\{\{\s*ga ?nominee\s*[\|\}][^\}]*\}\}/i))) { assess.extra.push('gan'); tempMatch = assess.reg[0].match(/\|\s*page\s*=\s*(\d+).*\|\s*status\s*=\s*\w+\b/i); if (tempMatch) assess.pageLink[0] = 'Talk:' + this.encodePageName(mw.config.get('wgPageName')) + '\/GA' + tempMatch[1]; }       // Current reviews of a status (FAR, FLRC, or GAR) else if ((assess.reg = text.match(/\{\{\s*featured[ _]article[ _]review\s*(?:[\|\}]\s*([^\|\}]*))?[^\}]*?\}\}/i))) { assess.extra.push('far'); if (assess.reg[1] && (articleName = this.decodeEntities($.trim(assess.reg[1])))) assess.pageLink[0] = 'Wikipedia:Featured_article_review\/' + articleName; } else if ((assess.reg = text.match(/\{\{\s*featured[ _]list[ _]removal[ _]candidates\s*(?:[\|\}]\s*([^\|\}]*))?[^\}]*?\}\}/i))) { assess.extra.push('flrc'); if (assess.reg[1] && (articleName = this.decodeEntities($.trim(assess.reg[1])))) assess.pageLink[0] = 'Wikipedia:Featured_list_removal_candidates\/' + articleName; } else if ((assess.reg = text.match(/\{\{\s*gar\/link\s*[\|\}][^\}]*\}\}/i))) { assess.extra.push('gar'); tempMatch = assess.reg[0].match(/\|\s*GARpage\s*=\s*(\d+).*\|/i); if (tempMatch) assess.pageLink[0] = this.getGARLink(this.encodePageName(mw.config.get('wgPageName')), tempMatch[1]); }       // Former statuses (FFA, FFL, or DGA) if ((assess.reg = text.match(/\|\s*currentstatus\s*=\s*ffa\b/i))) { tempMatch = text.match(/\|\s*action(\d+)\s*=\s*far\b/gi); actionNumber = tempMatch[tempMatch.length - 1].match(/\d+/); pageLinkFlag = true; assess.extra.push('ffa'); } else if ((assess.reg = text.match(/\|\s*action(\d+)\s*=\s*far\b/gi))               // This checks if the last FAR entry in ArticleHistory resulted in removal.                && text.match(RegExp(                    '\\|\\s*action' + assess.reg[assess.reg.length - 1].match(/\d+/)                        + 'result\\s*=\\s*removed\\b', 'i'                )) && assess.rating.search(/f[al]/i) == -1) { actionNumber = assess.reg[assess.reg.length - 1].match(/\d+/); pageLinkFlag = true; assess.extra.push('ffa'); } else if ((assess.reg = text.match(/\{\{\s*formerfa2?\b/i))) { assess.extra.push('ffa'); } else if ((assess.reg = text.match(/\|\s*currentstatus\s*=\s*ffl\b/i))) { assess.extra.push('ffl'); } else if ((assess.reg = text.match(/\{\{\s*ffl\s*[\|\}]/i))) { assess.extra.push('ffl'); } else if ((assess.reg = text.match(/\|\s*currentstatus\s*=\s*dga\b/i))) { tempMatch = text.match(/\|\s*action(\d+)\s*=\s*gar\b/gi); actionNumber = tempMatch[tempMatch.length - 1].match(/\d+/); pageLinkFlag = true; assess.extra.push('dga'); } else if ((assess.reg = text.match(/\{\{\s*d(elisted)?ga\s*[\|\}]/i))) { assess.extra.push('dga'); }       // Former nominations (former FAC, FLC, or GAN) else if ((assess.reg = text.match(/\|\s*action(\d+)\s*=\s*fac\b/gi))               && assess.rating.search(/f[al]/i) == -1) { actionNumber = assess.reg[assess.reg.length - 1].match(/\d+/); pageLinkFlag = true; assess.extra.push('ffac'); } else if ((assess.reg = text.match(/\|\s*currentstatus\s*=\s*ffac\b/i))) { assess.extra.push('ffac'); } else if ((assess.reg = text.match(/\{\{\s*fac?(failed|(\-|[ _]\?contested\)?)\s*[\|\}]/i))) { assess.extra.push('ffac'); } else if ((assess.reg = text.match(/\|\s*action(\d+)\s*=\s*flc\b/gi))               && assess.rating.search(/f[al]/i) == -1) { actionNumber = assess.reg[assess.reg.length - 1].match(/\d+/); pageLinkFlag = true; assess.extra.push('fflc'); } else if ((assess.reg = text.match(/\|\s*currentstatus\s*=\s*fflc\b/i))) { assess.extra.push('fflc'); } else if ((assess.reg = text.match(/\|\s*action(\d+)\s*=\s*gan\b/gi))               && assess.rating.search(/f[al]|(a\/)?ga/i) == -1) { actionNumber = assess.reg[assess.reg.length - 1].match(/\d+/); pageLinkFlag = true; assess.extra.push('fgan'); } else if ((assess.reg = text.match(/\|\s*currentstatus\s*=\s*fgan\b/i))) { assess.extra.push('fgan'); } else if ((assess.reg = text.match(/\{\{\s*f(ailed ?)?ga\s*[\|\}]/i))) { assess.extra.push('fgan'); }

// Looks for currently active peer reviews var peerReview; if ((peerReview = text.match(/\{\{\s*peer[_ ]?review\s*\|\s*archive\s*=\s*(\d+)\b/i))) { assess.review = 'Wikipedia:Peer_review/' + mw.config.get('wgPageName') + '/archive' + peerReview[1]; assess.activeReview = true; } else if (this.showOldPeerReviews) { // TODO: Add code for old peer reviews } else assess.review = null;

// Scans for the link associated with an action in ArticleHistory if (pageLinkFlag) { var linkPattern = RegExp('\\|\\s*action' + actionNumber + 'link\\s*=\\s*([^\\n\\|]+)\\s*\\|'); var linkMatch = text.match(linkPattern); assess.pageLink[1] = linkMatch ? this.decodeEntities(linkMatch[1]) : null; }

assess.exists = true; this.callHooks('getAssessment_after'); return assess; },   /**     * Parses an assessment object into the HTML and CSS code needed to update * the article header. If it doesn't recognize a part of the information * given, it will simply ignore it and mark as unassessed. * @param {Object} assess - assessment information for this article * @return {String} newClass - the CSS class corresponding to its assessment * @return {String} slogan - HTML giving (with a link) the main assessment * @return {String} info - HTML giving (with a link) additional information */   talkAssess: function talkAssess (assess) { this.callHooks('talkAssess_before');

var path = mw.config.get('wgArticlePath').replace('$1', ''); var assessLink = path + 'Wikipedia:Version_1.0_Editorial_Team/Assessment'; if (typeof assess.extra === 'undefined') assess.extra = ''; var extra = assess.extra, rating = assess.rating; var pageLink = assess.pageLink ? [this.encodePageName(assess.pageLink[0]), this.encodePageName(assess.pageLink[1])] : [null, null]; var peerReview = this.encodePageName(assess.review);

var info = this.getExtraInfo(extra, pageLink); var peerReviewText = this.addPeerReview(peerReview, assess.activeReview); if (peerReviewText) info.push(peerReviewText); var newClass, slogan;

if (rating == 'a' || rating == 'a/ga') { newClass = 'assess-a-text'; slogan = 'An A-class article'; if (rating == 'a/ga') { info.push('Also a good article.'); }       } else if (rating == 'ga') { newClass = 'assess-ga-text'; slogan = 'A good article' } else if (rating == 'b') { newClass = 'assess-b-text'; slogan = 'A B-class article'; } else if (rating == 'bplus') { newClass = 'assess-bplus-text'; slogan = 'A B-plus-class article'; } else if (rating == 'c') { newClass = 'assess-c-text'; slogan = 'A C-class article'; } else if (rating == 'start') { newClass = 'assess-start-text'; slogan = 'A start-class article'; } else if (rating == 'stub') { newClass = 'assess-stub-text'; slogan = 'A stub-class article'; } else if (rating == 'sl') { newClass = 'assess-sl-text'; slogan = 'A stub-class</a> list'; } else if (rating == 'list') { newClass = 'assess-list-text'; slogan = 'A list-class</a> article'; } else if (rating == 'dab') { newClass = 'assess-dab-text'; slogan = 'A disambiguation page</a>'; } else if (rating == 'setindex') { newClass = 'assess-setindex-text'; slogan = 'A ' + 'set index article</a>'; } else if (rating == 'redir') { newClass = 'assess-redir-text'; slogan = 'A redirect page</a>'; } else if (rating == 'fl') { newClass = 'assess-fl-text'; slogan = 'A featured list</a>'; } else if (rating == 'fa') { newClass = 'assess-fa-text'; slogan = 'A featured article</a>'; } else if (rating == 'cur') { newClass = 'assess-cur-text'; slogan = 'A current-class</a> article'; } else if (rating == 'future') { newClass = 'assess-future-text'; slogan = 'A future-class</a>' + ' article'; } else { newClass = 'assess-unassessed-text'; slogan = 'An unassessed</a> article'; }

this.callHooks('talkAssess_after'); return {newClass: newClass, slogan: slogan, info: info}; },   /**     * Creates an info string based on the assessment info and a page link. */   getExtraInfo: function getExtraInfo (extra, pageLink) { var info = []; var page = this.encodePageName(mw.config.get('wgPageName')); // Current nominations and reviews if (extra.indexOf('fac') != -1) { info.push(this.makeInfoString('Currently a', pageLink[0], 'Wikipedia:Featured_article_candidates/' + page, 'featured article candidate', null)); } else if (extra.indexOf('flc') != -1) { info.push(this.makeInfoString('Currently a', pageLink[0], 'Wikipedia:Featured_list_candidates/' + page, 'featured list candidate', null)); } else if (extra.indexOf('gan') != -1) { info.push(this.makeInfoString('Currently a', pageLink[0], 'Wikipedia:Good_article_nominations', 'good article nominee', null)); } else if (extra.indexOf('far') != -1) { info.push(this.makeInfoString('Currently undergoing', pageLink[0], 'Wikipedia:Featured_article_review/' + page, 'review', 'of its featured status')); } else if (extra.indexOf('flrc') != -1) { info.push(this.makeInfoString('Currently a', pageLink[0], 'Wikipedia:Featured_list_removal_candidates/' + page, 'candidate', 'for removal as a featured list')); } else if (extra.indexOf('gar') != -1) { info.push(this.makeInfoString(' Currently undergoing a', pageLink[0], 'Wikipedia:Good_article_reassessment', 'good article reassessment', '<\/span>', true)); }       // Past statuses and nominations if (extra.indexOf('ffa') != -1) { info.push(this.makeInfoString('A', pageLink[1], 'Wikipedia:Featured_article_review/' + page, 'former', 'featured article')); } else if (extra.indexOf('ffl') != -1) { info.push(this.makeInfoString('A', pageLink[1], 'Wikipedia:Featured_list_removal_candidates/' + page, 'former', 'featured list')); } else if (extra.indexOf('dga') != -1) { info.push(this.makeInfoString('A', pageLink[1], 'Wikipedia:Good_article_reassessment', 'delisted', 'good article')); } else if (extra.indexOf('ffac') != -1) { info.push(this.makeInfoString('A former', pageLink[1], 'Wikipedia:Featured_article_candidates/' + page, 'featured article candidate', null)); } else if (extra.indexOf('fflc') != -1) { info.push(this.makeInfoString('A former', pageLink[1], 'Wikipedia:Featured_list_candidates/' + page, 'featured list candidate', null)); } else if (extra.indexOf('fgan') != -1) { info.push(this.makeInfoString('A former', pageLink[1], 'Wikipedia:Good_article_nominations', 'good article nominee', null)); }       return info; },   /**     * Get the correct link for Good Article reassessments. These things require an    * additional AJAX request to determine whether it's a community or individual * reassessment. The trick is to assume it's a community reassessment, then * switch the link once the request returns if it's actually not. */   getGARLink: function getGARLink (articleName, reviewNumber) { var communityTitle = 'Wikipedia:Good_article_reassessment\/' + articleName + '\/' + reviewNumber, individualTitle = 'Talk:' + articleName + '\/GA' + reviewNumber, url = mw.config.get('wgServer') + mw.config.get('wgScriptPath') + '\/api.php?action=query&titles=' + communityTitle + '|' + individualTitle + '&prop=info&format=json'; this.ajaxMain(url, this.getGARLinkCallback, [communityTitle, individualTitle]); return communityTitle; },   /**     * Now we have the information back from the API and need to figure out if the * link needs to be changed. */   getGARLinkCallback: function getGARLinkCallback (request, altTitles) { if (request.readyState == 4 && request.status == 200) { var text = request.responseText; if (JSON && JSON.parse) { var query = JSON.parse(text)['query']; }           else return; var communityTitleNorm = altTitles[0], individualTitleNorm = altTitles[1]; var len = query['normalized'].length; for (var j = 0; j < len; j++) { switch (query['normalized'][j]['from']) { case altTitles[0]: communityTitleNorm = query['normalized'][j]['to']; break; case altTitles[1]: individualTitleNorm = query['normalized'][j]['to']; break; }           }            var noCommunityAssessment = false; for (var i = -1; i >= -2; i--) { if (query['pages'][i] && typeof query['pages'][i]['missing'] === 'string') { if (query['pages'][i]['title'] == individualTitleNorm) { // No individual assessment, no need to change anything. return; }                   else if (query['pages'][i]['title'] == communityTitleNorm) { // There's no community assessment, so flag it. noCommunityAssessment = true; }               }            }			var garLink = $('#assess-gar-link a:first'); if (noCommunityAssessment && garLink.length) {				// There's an individual assessment but no community assessment. Switch the link. garLink.attr('href', mw.config.get('wgArticlePath').replace('$1', '') + altTitles[1]); }       }    },    /**     * Creates the peer review text from an info string, if a peer review was detected earlier. */   addPeerReview: function addPeerReview (peerReview, activeReview) { var reviewText = null, path = mw.config.get('wgArticlePath').replace('$1', ''); if (peerReview) { reviewText = (activeReview               ? 'Currently being peer reviewed</a>.'                : 'Previously peer reviewed</a>.'); reviewText = ' ' + reviewText + ' '; }       return reviewText; },   /**     * Updates article header with new assessment information by giving it a new * class (for style information such as color) and altering the tagline below * it to state the assessment found. * @param {String} newClass - the CSS class name added to the article header * @param {String} slogan - italicized text prepended to the tagline, showing *       the article's main assessment * @param {String} info - additional assessment info appended to the tagline * @static */   update: function update (newClass, slogan, info) { var firstHeading = $('h1:first'); var siteSub = ' ' + slogan + '<\/span> from Wikipedia, the free encyclopedia'; if (info && info.length > 0) siteSub += '. ' + (typeof info.join === 'undefined' ? info.toString : info.join(' ')) + '<\/span>'; firstHeading.addClass((typeof newClass.join === 'undefined' ? newClass.toString : newClass.join(' '))); // add newClass as additional class(es) $('#siteSub').html(siteSub); },   /**     * Creates a string formatted for the 'info' parameter in the update method. * @param start - text at the beginning of the string, before the link * @param pageLink - a link to the target page * @param defLink - the backup page link if !pageLink * @param linkText - the text of the link * @param end - text after the link * @return {String} output - the info string * @static */   makeInfoString: function makeInfoString (start, pageLink, defLink, linkText, end, noEndSpace) { var output; // path is usually just '/wiki/', but it's different on secure.wikimedia.org var path = mw.config.get('wgArticlePath').replace('$1', ''); var page = pageLink ? path + pageLink : (defLink ? path + defLink : null); start = start ? start.toString + ' ' : ''; linkText = linkText ? linkText.toString : ''; end = (end ? (noEndSpace ? '' : ' ') + end.toString + '.' : '.'); output = start + (page ? '' : ' \/>') : )           + linkText + ((page && linkText) ? '<\/a>' : ) + end; return output; },   /**     * Encodes the URL of a Wikipedia page for use in the talkAssess method. * @param {String} inputText - the unencoded full page name * @return {String} outputText - the encoded page name * @static */   encodePageName: function encodePageName (inputText) { if (!inputText) return null; var outputText = encodeURIComponent(inputText); while (outputText != null && outputText.match(/(\%20|\%2F|\%253A)/i)) { outputText = outputText.replace(/\%20/i, '_'); // unescape spaces for readability outputText = outputText.replace(/\%2F/i, '\/'); // %2F must be unescaped outputText = outputText.replace(/\%253A/i, ':'); // "%253A" for special cases such as Metroid: Other M }       return outputText; },   callHooks: function callHooks (hook) { for (funct in this[hook]) { this[hook][funct].call(this); }   },    addHook: function addHook (hook, funct) { if (typeof this[hook] === 'undefined') this[hook] = []; this[hook][this[hook].length] = funct; return this; },   /**     * Decodes all HTML entities in the string provided. */   decodeEntities: function decodeEntities (str) { var t = document.createElement("textarea"); t.innerHTML = str; return t.value; } }

// Implement Array.indexOf for older browsers that don't have it if (!Array.prototype.indexOf) { Array.prototype.indexOf = function indexOf (elt, from) { var len = this.length >>> 0; var from = Number(arguments[1]) || 0; from = (from < 0) ? Math.ceil(from) : Math.floor(from); if (from < 0) from += len; for (from < len; from++) { if (from in this && this[from] === elt) return from; }       return -1; }; }

/** * Initializes the script on page load */ if (mw.config.get('wgNamespaceNumber') == 0 && (mw.config.get('wgAction') == 'view' || mw.config.get('wgAction') == 'purge') && !mw.util.getParamValue('printable') && mw.config.get('wgPageName') != 'Main_Page' ) {	$(document).ready(function	{		assessment.init;	}); }