Welcome to the DFO World Wiki. With many major updates since the release of DFO, many items are missing. Visit Item Database Project to learn more.
Please remember to click "show preview" before saving the page.
Thanks for the updated logo snafuPop!
MediaWiki:Gadget-libCat.js
Note: After saving, you may have to bypass your browser's cache to see the changes.
- Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
- Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
- Internet Explorer: Hold Ctrl while clicking Refresh, or press Ctrl-F5
- Opera: Go to Menu → Settings (Opera → Preferences on a Mac) and then to Privacy & security → Clear browsing data → Cached images and files.
/** * Library for batch-editing categories * NOUI: This is a library; it does not create a User Interface * * It has several dependencies, most notably to gadget-libAPI * (which prevents too many simultaneous requests, see [[:mw:API:Etiquette]]) * and to gadget-libWikiDOM, a wikitext parser * * * @rev 1 (2013-05-04) * @author Rillke, 2013 * @license This software is quadruple licensed. You may use it under the terms of GPL v.3, LGPL v.3, CC-By-SA 3.0, GFDL 1.2 */ // List the global variables for jsHint-Validation. Please make sure that it passes http://jshint.com/ // Scheme: globalVariable:allowOverwriting[, globalVariable:allowOverwriting][, globalVariable:allowOverwriting] /*global jQuery:false, mediaWiki:false*/ // Set jsHint-options. You should not set forin or undef to false if your script does not validate. /*jshint forin:true, noarg:true, noempty:true, eqeqeq:true, bitwise:true, strict:true, undef:true, curly:false, browser:true, smarttabs:true*/ (function($, mw) { 'use strict'; mw.messages.set({ 'cbe-summary-remove': "$1: Removing from $2", 'cbe-summary-add': "$1: Adding $2", 'cbe-summary-move': "$1: Moving from $2 to $3", 'cbe-summary-copy': "$1: Copying from $2 to $3" }); var nsNumber = mw.config.get('wgNamespaceNumber'), formattedNS = mw.config.get('wgFormattedNamespaces'), nsIDs = mw.config.get('wgNamespaceIds'), _msg = function(/*params*/) { var args = Array.prototype.slice.call(arguments, 0); args[0] = 'cbe-' + args[0]; args[args.length] = 'libCat'; return mw.message.apply(this, args).parse(); }; /** * Representing a "category" * * @param {mw.libs.Cat} o * @param {internal.cfg} c Configuration associated with this category(-change) * @param {string} s name of the category * with namespace assumed * only important to be full pagename if category carries a duplicate ns like "Category:Category:Xyz" * @constructor */ function Category(o, c, s) { this.parent = o; this.title = s.replace(o.prefixRegExp, ''); this.name = internal.prefixNamespace(c, this.title); this.markup = internal.markupify(c, this.name); } Category.fn = Category.prototype = $.extend(Category.prototype, { getTitle: function() { return this.title; }, getName: function() { return this.name; }, getMarkup: function() { return this.markup; }, getRegExp: function() { return this.parent.fullRegExp(this.getTitle()); } }); /** * These methods will be used to augment the list of * categories to add or remove * Modifying Array.prototype is considered evil by some * JS experts */ var listAugmentationMethods = { joinCats: function(delim) { return $.map(this, function(cat) { return cat.getMarkup(); }).join(delim); }, cloneCats: function() { var c = this.slice(0); c.join = internal.joinCats; return c; } }; var internal = { cfg: { tool: 'libCat', fallbackCat: 'Category', nsCat: 14, moveAddIfNotExist: false, removeDupes: true, addDupes: false, editArgs: {}, // This way you can pass arguments like "bot" failConditions: { failedEditRatio: 1 }, retryConditions: { editConflict: true, count: 3 }, summary: { // Whether to append an automatically generated edit summary; // if false, only adds summary if it is not specified auto: true, copyFrom: (14 === nsNumber) ? mw.config.get('wgPageName').replace(/_/g, ' ') : '' } }, /** * Returns true if s is a string, otherwise false */ isString: function(s) { return 'string' === typeof s; }, /** * Returns true if x is undefined, otherwise false */ isUndefined: function(x) { return 'undefined' === typeof x; }, /** * getLocalizedRegex: Copyright by [[User:Lupo]] * Taken from HotCat and slightly altered */ getLocalizedRegex: function(namespaceNumber, fallback) { var wikiTextBlank = '[\\t _\\xA0\\u1680\\u180E\\u2000-\\u200A\\u2028\\u2029\\u202F\\u205F\\u3000]+', wikiTextBlankRE = new RegExp(wikiTextBlank, 'g'); var createRegexStr = function(name) { if (!name || name.length === 0) return ""; var regex_name = ""; for (var i = 0; i < name.length; i++) { var initial = name.substr(i, 1); var ll = initial.toLowerCase(); var ul = initial.toUpperCase(); if (ll === ul) { regex_name += initial; } else { regex_name += '[' + ll + ul + ']'; } } return regex_name.replace(/([\\\^\$\.\?\*\+\(\)])/g, '\\$1').replace(wikiTextBlankRE, wikiTextBlank); }; fallback = fallback.toLowerCase(); var canonical = formattedNS[namespaceNumber].toLowerCase(), RegexString = createRegexStr(canonical); if (fallback && canonical !== fallback) RegexString += '|' + createRegexStr(fallback); for (var catName in nsIDs) { if (nsIDs.hasOwnProperty(catName)) { if (this.isString(catName) && nsIDs[catName] === namespaceNumber && catName.toLowerCase() !== canonical && catName.toLowerCase() !== fallback) { RegexString += '|' + createRegexStr(catName); } } } return ('(?:' + RegexString + ')'); }, /** * Normalize category-lists and augment them * * @param {mw.libs.Cat} o instance of the libCat object * @param {Object} p parameters - contains categories to remove, add, the titles, edit summary etc. (batch) * @param {internal.cfg} c configuration for this batch * @param {string} l type of list to process ['add'|'remove'] * * @return {Object} the processed parameters */ processCatList: function(o, p, c, l) { if (this.isString(p[l])) p[l] = [p[l]]; if (!$.isArray(p[l])) p[l] = []; p[l] = $.map(p[l], function(catname) { return new Category(o, c, catname.replace(o.prefixRegExp, '')); }); $.extend(p[l], listAugmentationMethods); }, /** * Add edit summary if appropriate * * @param {Object} p parameters - contains categories to remove, add, the titles, edit summary etc. (batch) * @param {internal.cfg} c configuration for this batch * @param {string} s edit summary to [possibly] add to the provided summary * * @return {Object} the processed parameters */ addSummary: function(p, c, s) { if (p.summary && !c.summary.auto) return; p.summary = p.summary || ''; p.summary = $.trim(p.summary + ' ' + s); }, prefixNamespace: function(c, s) { return [formattedNS[c.nsCat], ':', s].join(''); }, /** * Returns Wiki-Markup that can be used to categorize into the provided category catName * In parser-language: Turn a plain text into a link. * * @param {string} catName Full name of the category * * @return {string} */ markupify: function(c, catName) { return ['[[', catName, ']]'].join(''); }, /** * Normalize input * and determine what kind of operation we'll run * * @param {mw.libs.Cat} o instance of the libCat object * @param {Object} p parameters - contains categories to remove, add, the titles, edit summary etc. (batch) * @param {internal.cfg} c configuration for this batch * * @return {Object} the processed parameters */ processArgs: function(o, p, c) { // Remove "Category:" from the set of rules // and ensure data is of type Array internal.processCatList(o, p, c, 'remove'); internal.processCatList(o, p, c, 'add'); // Create auto-summary if (1 === p.remove.length && 1 === p.add.length) { internal.addSummary(p, c, _msg('summary-move', c.tool, p.remove[0].getMarkup(), p.add[0].getMarkup())); p.type = 'move'; } else if (c.summary.copyFrom && 0 === p.remove.length) { internal.addSummary(p, c, _msg('summary-copy', c.tool, c.currentPageCat.getMarkup(), p.add[0].getMarkup())); p.type = 'copy'; } else if (0 === p.remove.length && p.add.length > 0) { internal.addSummary(p, c, _msg('summary-add', c.tool, p.add.join(', '))); p.type = 'add'; } else if (0 === p.add.length && p.remove.length > 0) { internal.addSummary(p, c, _msg('summary-remove', c.tool, p.remove.join(', '))); p.type = 'remove'; } if (internal.isString(p.titles)) p.titles = p.titles.split('|'); if (!$.isArray(p.titles)) throw new Error('libCat: Invalid arguments supplied. params.titles must be of type Array or String'); return p; } }; var init = function(cfg, o) { o.localizedRegex = internal.getLocalizedRegex(cfg.nsCat, cfg.fallbackCat); o.prefixRegExp = o.getPrefixRegExp(); cfg.currentPageCat = new Category(o, cfg, cfg.summary.copyFrom); }; mw.libs.Cat = function(cfg) { this.cfg = cfg; }; mw.libs.Cat.prototype = $.extend(true, mw.libs.Cat.prototype, { /** * Batch edits categories according to the specified * parameters * * @example * var params = { * remove: [], // Array or string containing categories to be removed * add: [], * titles: [], // Array of titles to work on or a string of page separated by a pipe (|) character * summary: '', // String -- Reason for doing so * beforeSave: function() {} // Callback. First argument is the text to be saved. Must return the text to be finally saved. * }; * new mw.libs.Cat().batchEdit( params ).progress(function() { * alert('One page edited'); * }).done(function() { * alert('All pages edited'); * }); * * @param params {object} Object containing information about categories to add or remove and an edit summary * @param cfg {object} Possiblity to overwrite the default configuration * * @return {$.Deferred} jQuery Defferred-Object. * Arguments are passed to * -notify -> progress: (1) {string} status, (2) {string} title, (3) {Object} stats|ft|<nothing> * -resolve -> done: (1) {Object} stats, (2) {Object} failedTitles * -reject -> fail: (1) {string} reason */ batchEdit: function(params, cfg) { var _t = this, $def = $.Deferred(), pending = 0, queryPending = false, retriedTitles = {}, stats = { done: 0, outstanding: params.titles.length, failed: 0, percentDone: function() { return (this.done / (this.done + this.outstanding)) * 100; } }; // Merge configuration: for this batch to the config of the mw.libs.Cat instance to the default config // Empty object prevents changing default config cfg = $.extend(true, {}, internal.cfg, _t.cfg, cfg); $.extend($def, { failedTitles: {}, stats: stats }); // Initialize RegExps, create create category object for current category init(cfg, this); // Process params (passing configuration); Normalze input params = internal.processArgs(this, $.extend(true, {}, params), cfg); var oneDone = function() { stats.done++; stats.outstanding--; return stats; }; var possiblyNextChunk = function() { if (pending < 3 && params.titles.length && !queryPending) { fetch(); } else if (0 === params.titles.length && 0 === pending) { $def.resolve(stats, $def.failedTitles); } }; var editPage = function(pg, rv, txt) { if ($.trim(rv['*']) === $.trim(txt)) { $def.notify('nochange', title, oneDone()); possiblyNextChunk(); return; } pending++; var title = pg.title; mw.libs.commons.api.editPage($.extend(true, {}, { title: title, editType: 'text', text: txt, summary: params.summary, starttimestamp: pg.starttimestamp, basetimestamp: rv.timestamp, cb: function() { pending--; $def.notify('done', title, oneDone()); possiblyNextChunk(); }, errCb: function(txt, r) { pending--; var rt = retriedTitles[title], dontRetry; dontRetry = function(ev) { // do not retry this title var ft = { reason: txt, response: r, evidence: 'retryConditions.' + ev }; $def.failedTitles[title] = ft; stats.failed++; stats.outstanding--; $def.notify('failedTitle', title, ft); }; if (rt) { rt++; } else { rt = 1; } retriedTitles[title] = rt; if (txt.indexOf('editconflict') !== -1 && !cfg.retryConditions.editConflict) { dontRetry('editConflict'); } else if (rt > cfg.retryConditions.count) { dontRetry('count'); } else { $def.notify('retryTitle', title); params.titles.push(title); } possiblyNextChunk(); } }, cfg.editArgs)); }; var _contensAvailable = function(r) { queryPending = false; var pgs = r.query.pages; $.each(pgs, function(i, pg) { // Page disappeared? if (!pg.revisions) return; var allCats = $.map(pg.categories || [], function(cat) { return new Category(_t, cfg, cat.title).getTitle(); }), rv = pg.revisions[0], txt = _t.change(params, cfg, rv['*'], allCats); if ($.isFunction(params.beforeSave)) txt = $.trim(params.beforeSave(txt, pg, rv)); // Page blanking is blocked by AbuseFilter if (txt) editPage(pg, rv, txt); }); // Check if we actually scheduled edits possiblyNextChunk(); }; // Fetch page content var fetch = function() { queryPending = true; mw.libs.commons.api.$query({ action: 'query', prop: 'revisions|info|categories', cllimit: 'max', intoken: 'edit', titles: params.titles.splice(0, 5).join('|'), rvprop: 'content|timestamp' }, { // Prevent any caching method: 'POST' }).done(_contensAvailable).fail(function() { // Should not happen since libAPI is very error-tolerant $def.reject('libCat: Fetching page contents failed'); }); }; possiblyNextChunk(); return $def; }, change: function(params, cfg, txt, allCats) { allCats = allCats || []; var dom = mw.libs.wikiDOM.parser.text2Obj(txt), // Get the list of categories in reverse order cats = (dom.nodesByType.category || []).reverse(), didReplacement = false, catCount = cats.length, toAdd = params.add.clone(), currentCatRE, catPart, toAppend; if ('move' === params.type) { // FIXME: Respect cfg.addDupes toAppend = '\n' + toAdd[0].getMarkup(); if (catCount > 0) { currentCatRE = params.remove[0].getRegExp(); $.each(cats, function(i, cat) { // Cannot and want not edit categories built with templates or templateargs // or other fancy stuff in it. catPart = cat.parts[0][0]; if (!internal.isString(catPart)) return; // Are you the cat I want to catch? if (currentCatRE.test(catPart)) { if (didReplacement && cfg.removeDupes) { // If we already replaced the category, remove duplicate stuff // nodes with 'deleted' type are simply ignored by the DOM parser // and consequently this category won't be added again when // transforming DOM-Object -> text cat.type = 'deleted'; } else { cat.parts[0][0] = toAdd[0].getName(); didReplacement = true; } } }); if (cfg.moveAddIfNotExist) { cats[0].after(toAppend); didReplacement = true; } } if (didReplacement) { txt = mw.libs.wikiDOM.parser.obj2Text(dom); } else if (cfg.moveAddIfNotExist) { // No cats on the page --> Append one txt += toAppend; } } else { if (!cfg.addDupes) { $.each(toAdd, function(i, cat2Add) { var title = cat2Add.getTitle(); if ($.inArray(title, allCats) > -1) { toAdd.splice(i, 1); } }); } if (0 !== toAdd.length) { toAppend = '\n' + toAdd.join('\n'); if (0 === catCount) { dom.append(toAppend); } else { cats[0].after(toAppend); } } $.each(params.remove, function(i, cat2Remove) { currentCatRE = cat2Remove.getRegExp(); $.each(cats, function(i, cat) { // Cannot and want not edit categories built with templates or templateargs // or other fancy stuff in it. catPart = cat.parts[0][0]; if (!internal.isString(catPart)) return; // Are you the cat I want to catch? if (currentCatRE.test(catPart)) { cat.type = 'deleted'; } }); }); txt = mw.libs.wikiDOM.parser.obj2Text(dom); } return txt; }, localizedRegex: null, /** * fullRegExp: Copyright by [[User:Lupo]] * Taken from HotCat and slightly altered */ fullRegExp: function(category) { // Build a regexp string for matching the given category: // trim leading/trailing whitespace and underscores category = category.replace(/^[\s_]+/, '').replace(/[\s_]+$/, ''); // escape regexp metacharacters (= any ASCII punctuation except _) category = $.escapeRE(category); // any sequence of spaces and underscores should match any other category = category.replace(/[\s_]+/g, '[\\s_]+'); // Make the first character case-insensitive: var first = category.substr(0, 1); if (first.toUpperCase() !== first.toLowerCase()) category = '[' + first.toUpperCase() + first.toLowerCase() + ']' + category.substr(1); // Compile it into a RegExp that matches MediaWiki category syntax (yeah, it looks ugly): return new RegExp('^[\\s_]*' + this.localizedRegex + '[\\s_]*:[\\s_]*' + category + '[\\s_]*$', ''); }, prefixRegExp: '', getPrefixRegExp: function() { return new RegExp('^[\\s_]*' + this.localizedRegex + '[\\s_]*\\:[\\s_]*', ''); } }); }(jQuery, mediaWiki));