User:Amalthea/Twirl.js

var css = 'form.twirl, form.twirl * {'+ ' padding:0;'+ ' margin:0;'+ '}'+ ''+  'form.twirl {'+ ' font-size:120%;'+ ' min-width:18em;'+ ' min-height:10em;'+ '}'+ ''+  'form.twirl fieldset {'+ ' border:none;'+ '}'+ ''+  'form.twirl > fieldset > div {'+ ' width:100%;'+ ' position:relative;'+ ' margin-top:.3em;'+ '}'+ ''+  'form.twirl div label.label {'+ ' font-weight:bold;'+ ' display:block;'+ ' float:left;'+ ' width:9em;'+ ' text-align:right;'+ '}'+ ''+  'form.twirl div label.checkbox, form.twirl div fieldset, form.twirl div div.textareawrapper, form.twirl div div.inputwrapper {'+ ' display:block;'+ ' position:relative;'+ ' left:0;'+ ' right:0;'+ ' margin-left:9.5em'+ '}'+ ''+  'form.twirl div div.textareawrapper textarea {'+ ' min-width:25em;'+ ' width:100%;'+ ' height:100%;'+ ' min-height:10em;'+ '}'+ ''+  'form.twirl div div.inputwrapper input.text, form.twirl div div.inputwrapper select {'+ ' width:100%;'+ '}'; appendCSS(css);

/* * TWIRL * Yet another MediaWiki tool framework. * * If this ever becomes useful enough that you want to use it on another WikiMedia project, talk to me about internationalization. */

/* * This script is under heavy development. Heavy, meaning that it is likely * to be radically rewritten at any time. If you still want to use it, do * one of two things: * - Notify me, so that I'm aware of it and can handle it appropriately. * - Import one specific revision of the script instead of the latest one */

(function{

if (!window.Twirl) Twirl = {}; if (Twirl.Bits) return;

//import helper scripts if (!window.jQuery) {	//"http://ajax.googleapis.com/ajax/libs/jqueryui/1.8.3/themes/base/jquery-ui.css" //"http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js" //"http://ajax.googleapis.com/ajax/libs/jqueryui/1.8.3/jquery-ui.min.js" mw.loader.load( window.TwirlDebug && window.TwirlDebug.UnminifiedJQuery ?'http://bits.wikimedia.org/skins-1.5/common/jquery.js':'http://bits.wikimedia.org/skins-1.5/common/jquery.min.js'); } if (!window.jQuery.ui) { mw.loader.load( window.TwirlDebug && window.TwirlDebug.UnminifiedJQuery ?'http://bits.wikimedia.org/w/extensions/UsabilityInitiative/js/js2stopgap/jui.combined.js':'http://bits.wikimedia.org/w/extensions/UsabilityInitiative/js/js2stopgap/jui.combined.min.js'); importStylesheetURI( 'http://bits.wikimedia.org/w/extensions/UsabilityInitiative/css/vector/jquery-ui-1.7.2.css?1.7.2y' ); }

Twirl.Bits = { //Initiate an asynch API query. It takes on parameter defining the exact query // The ajaxQuery object can contain the following key-value pairs // parameters: An object containing the MediaWiki API parameter, e.g.: {list:'categorymembers'} // onHttpError(errorCode, query): callback if the HTTP call was unsuccessful // onApiError(errorCode, JsonResult, query): callback if the API returned an error code. // onSuccess(JsonResult, query): callback if the ajax call was successful // // If error methods are not defined, control will *not* fall through. They will only be logged. // Handle your errors! Api : function(query) {     var req = sajax_init_object; req.open('POST', mw.config.get('wgServer') + mw.config.get('wgScriptPath') + "/api.php", true);

req.setRequestHeader('Accept', 'text/javascript, text/html, application/xml, text/xml, */*'); req.setRequestHeader('Content-type', 'application/x-www-form-urlencoded; charset=UTF-8');

var reqWrapper = {req:req, query:query, finished:false};

setTimeout(function { Twirl.Bits.__apiOnComplete(reqWrapper, 418); }, 10000); //manually timeout every request after 10 seconds, teapot style. req.onreadystatechange = function {       //complete? if (reqWrapper.req.readyState != 4) return; Twirl.Bits.__apiOnComplete(reqWrapper, reqWrapper.req.status); };

var queryParas = jQuery.extend({action:'query'}, query.parameters); queryParas.format='json'; //we insist. delete queryParas.callback; //we insist.

req.send(Twirl.Bits.BuildQuery(queryParas)); }, __apiOnComplete : function(reqWrapper, httpResult) {     if (reqWrapper.finished) return; reqWrapper.finished = true;

if (httpResult<200 || httpResult>=300) {       if (reqWrapper.query.onHttpError) reqWrapper.query.onHttpError(httpResult, reqWrapper.query); else Twirl.Bits.Error("HTTP error $1", httpResult); return; }

var apiResult; try {       apiResult = eval('('+reqWrapper.req.responseText+')'); //TODO: Use parseJSON, as soon as bits.wikimedia updates! }     catch(e) {       apiResult = {"error": {"code": "invalid_API_response", "info": "Twirl.Bits recieved invalid JSON from the API", "*": "" }}; }

if (apiResult.error) {       if (reqWrapper.query.onApiError) reqWrapper.query.onApiError(apiResult.error.code, apiResult, reqWrapper.query); else Twirl.Bits.Error("API error $1", apiResult.error.code); return; }     if (reqWrapper.query.onSuccess) reqWrapper.query.onSuccess(apiResult, reqWrapper.query);

reqWrapper.req.onreadystatechange = Twirl.Bits.Void; }, BuildQuery : function(queryDict) //based on User:AzaToth/morebits.js {     if (typeof queryDict == 'string') return queryDict; var parameterArray = Array; var editToken; //ensure that the editToken is the last parameter transmitted for( var key in queryDict ) {       if( typeof queryDict[key] == 'undefined' ) continue; var value = queryDict[key]; if( value instanceof Array ) value = value.join('|'); //a list of subvalues? if( key == 'wpEditToken' ) editToken = value; else parameterArray.push( encodeURIComponent(key) + '=' + encodeURIComponent(value) ); }     if( typeof editToken != 'undefined' ) parameterArray.push( 'wpEditToken=' + encodeURIComponent(editToken) ); var result = parameterArray.join('&'); return result; }, // Simple string formatting, turns  ("Foo $2 Bar $1!", 42, 'a') into "Foo a Bar 42!". Format : function(s) {     if (arguments.length<2) return s;      var count = arguments.length-1; for (var i=1; i<=count; i++) s = s.replace(new RegExp('\\$' + i, 'g' ), arguments[i]); return s;   }, Void : function {}, LoadPage : function(page) {     document.location = mw.config.get('wgServer') + mw.config.get('wgArticlePath').replace('$1',encodeURIComponent(page.replace(" ",'_')).replace('%2F','/').replace('%3A',':') ); }, Log : function { if (!(window.console && window.console.log)) return; message = Twirl.Bits.Format.apply(Twirl.Bits, arguments); window.console.log(message); }, Error : function { message = Twirl.Bits.Format.apply(Twirl.Bits, arguments); if (window.console && window.console.error) window.console.error(message); alert(message); }, JobResult : function(name) { return new Twirl.Bits.Jobs.JobStub(name); }, AddPortletLink : function(moduleName, callbackName) {     mw.util.addPortletLink(        Twirl.Config[moduleName].PortletId || Twirl.Config.Twirl.PortletId,        "javascript:Twirl."+moduleName+"."+(callbackName||"Start")+"",        Twirl.Config[moduleName].TabName || 'Unnamed',        "twirl-"+moduleName.toLowerCase || ,        Twirl.Config[moduleName].TabTitle || ,        Twirl.Config[moduleName].AccessKey || ''      ); } };

Twirl.Bits.Jobs = { //map job name to array of jobs depending on it _dependencies : {}, //map job name to job _jobs: {}, //Define a job. //You should not hold on to job settings, or try to modify them afterwards. If you do, its behaviour is undefined. // The Add function takes four parameters: // * A string, the name of the job // * A function, the job function. // * A array, optional, the function parameters // * A map, optional, the job settings // The name must be the first parameter, the order in which you provide the rest is up to you (for now). // Readability might be best if you pass name, funcParas, settings, func, since this clearly shows the job dependencies, // and the job function might be anonymous so it could be lengthy, thus hard to find any arguments or settings after the function Add : function(name, p1, p2, p3) {     var func = jQuery.isFunction(p3)?p3:(jQuery.isFunction(p2)?p2:p1); var funcParas = jQuery.isArray(p3)?p3:(jQuery.isArray(p2)?p2:p1); var settings = typeof p3 == "object" && !jQuery.isArray(p3) ? p3 : (typeof p2 == "object" && !jQuery.isArray(p2) ? p2 : p1); if (Twirl.Bits.Jobs._jobs[name]) throw Twirl.Bits.Format("Job '$1' is already defined.", name);

//== check if it's onfailrestart and nosideeffects settings are compatible with the current job graph ==

//TODO!

// == create a job object == var job = new Twirl.Bits.Jobs.Job(name, func, funcParas, settings);

// == hook it into the job graph == //first, gather all dependencies of this job var dependencies = Twirl.Bits.Jobs.__dependencies(job);

//then, write them into the map of dependencies, and check if there are any that aren't already fulfilled var dependenciesSatisfied = true; for (var dep in dependencies) {       if (!(Twirl.Bits.Jobs._dependencies[dep])) Twirl.Bits.Jobs._dependencies[dep] = []; Twirl.Bits.Jobs._dependencies[dep].push( job.Name ); dependenciesSatisfied &= Twirl.Bits.Jobs._jobs[dep] && Twirl.Bits.Jobs._jobs[dep].Status == "success"; }

Twirl.Bits.Jobs._jobs[name] = job;

// == attempt to run it if it has no unfinished dependencies == if (dependenciesSatisfied) job.Start; else Twirl.Bits.Log("Dependencies of $1 not satisfied, will wait", job.Name);

}, Success : function(job, result) {     if (this.__checkJobGroupIsBacktracking(job)) return; //if we are currently backtracking due to a recoverable failure, discard this job result, and possibly start the backtracking restart point

//the given job succeeded // store the result with the job

job.Result = result; job.Status = "success";

// check the dependancy graph which jobs depended on this one //   for each, check if all dependencies have finished. If so, start it. if (Twirl.Bits.Jobs._dependencies[job.Name]) {       var dependents = Twirl.Bits.Jobs._dependencies[job.Name]; for(var i =0; i<dependents.length; i++) {         var subJob = Twirl.Bits.Jobs._jobs[dependents[i]]; if (Twirl.Bits.Jobs.__dependenciesSatisfied(subJob)) subJob.Start; }     }    },  //Mark this job as failed. Fail : function(job, info, fatal) {     if (this.__checkJobGroupIsBacktracking(job)) return; //if we are currently backtracking due to a recoverable failure, discard this job result, and possibly start the backtracking restart point

//job failed //     //// if a job failed non-fatally, i.e. an immediate retry could work, increase the retry counter; if it's not higher than the max, Reinit the job, and restart it

job.Retries++; if (!fatal && job.Retries<=job.Settings.retries) {       job.Reinit; Twirl.Bits.Jobs.__start(job); return; }

//if fatal, or no more retries, this job failed, terminally // if onfailrestart is set, then: //   beginning from the onfailrestart job, process it and all of its dependencies that have started to run or that are finished //     If they are running, set their backtracking-abort property, and add them to the restart-root's backtracking-waiting list //     If they are finished, reinitialize them //     If the backtracking-waiting list of the restart-root is empty, start the restart-root.

if (job.Settings.onfailrestart) {       job.Reinit;

job.Settings.onfailrestart.BacktrackingWaitList = {}; job.Settings.onfailrestart.BacktrackingWaitCount = 0;

var dependentJobs = [job.Settings.onfailrestart]; //start with the job we are going to backtrack to. while(dependentJobs.length>0) {         var dependentJob = dependentJobs[dependentJobs.length-1];

switch(dependentJob.Status) {           case "waiting": //hasn't started yet, no need to do anything break; case "success": //gone its course: reinit, and process the dependencies. dependentJob.Reinit; dependentJob.Retries = 0; dependentJobs = $.merge(dependentJobs, Twirl.Bits.Jobs.__dependencies(dependentJob)); break; case "fail": //good news everyone, it gets another chance. No dependencies will have started dependentJob.Reinit; dependentJob.Retries = 0; break; case "running": //this job is currently running. Mark it so that it knows to discard its result, and note it down at the onfailrestart job dependentJob.BacktrackingJob = job.Settings.onfailrestart; job.Settings.onfailrestart.BacktrackingWaitList[dependentJob.Name] = true; job.Settings.onfailrestart.BacktrackingWaitCount++; break; }       }

var onfailrestartJob = job.Settings.onfailrestart; delete job.Settings.onfailrestart; //invalidate the onfailrestart setting: If it fails again, we don't want to try again. //TODO: That's hacky. We'd rather should use the normal retry counter for this!

//start it if we don't have to wait for running jobs. if (onfailrestartJob.BacktrackingWaitCount == 0) {         onfailrestartJob.Reinit; //get rid of the backtracking variables. Reiniting it doesn't hurt. onfailrestartJob.Start; }       //otherwise, once all jobs we're waiting have finished it will be started

}     else job.Status = "failure"; //can't be helped, otherwise. //TODO: If we are permafailing this job, we need to log it so that the user knows that processing has stopped, and that possibly only some jobs with sideeffects have finished. He needs to clean up manually. }, __checkJobGroupIsBacktracking : function(job) {     //  if the jobs backtracking property is set to some job then //   discard the result //   delete the backtracking property //   Reinitialize the job //   remove us from the backtracking root job's backtracking-waiting property; If it's empty, then start it.

if (!(job.BacktrackingJob)) return false; var backtrackJob = job.BacktrackingJob; job.Reinit; dependentJob.Retries = 0; delete backtrackJob.BacktrackingWaitList[job.Name]; backtrackJob.BacktrackingWaitCount--; if (backtrackJob.BacktrackingWaitCount==0) {       backtrackJob.Reinit; //get rid of the backtracking variables. Reiniting it doesn't hurt. backtrackJob.Start; }   },  __start : function(job) {     Twirl.Bits.Log("Starting job '$1'", job.Name);

if (job.Status != "waiting") throw Twirl.Bits.Format("Attempted to start job '$1', but its status wasn't 'waiting'", job.Name); // == just call the defined job function with the arguments, it will take care of the rest == //we assume that all dependencies have been resolved to our liking. var args = []; for (var i=0; i<job.FuncParas.length; i++) {       var arg = job.FuncParas[i]; if (arg instanceof Twirl.Bits.Jobs.Job) args.push(arg.Result); else if (arg instanceof Twirl.Bits.Jobs.JobStub) args.push(Twirl.Bits.Jobs._jobs[arg.Name].Result); else args.push(arg); }     job.Status = "running"; try {       job.Func.apply(job, args); }     catch(e) {       Twirl.Bits.Error("Exception during execution of '$1': '$2'.", job.Name, e); job.Fatal(e); }   },  //returns a dictionary of dependencies of the given job. __dependencies : function(job) {     var dependencies = {}; for (var i=0; i<job.Settings.waitfor.length; i++) dependencies[job.Settings.waitfor[i]] = true; for (var i=0; i<job.FuncParas.length; i++) {       var dep = job.FuncParas[i]; if ( !( (dep instanceof Twirl.Bits.Jobs.Job) || (dep instanceof Twirl.Bits.Jobs.JobStub) ) ) continue; dependencies[dep.Name] = true; }     return dependencies; }, __dependenciesSatisfied : function(job) {     var dependenciesSatisfied = true; for (var dep in Twirl.Bits.Jobs.__dependencies(job)) {       dependenciesSatisfied &= Twirl.Bits.Jobs._jobs[dep] && Twirl.Bits.Jobs._jobs[dep].Status == "success"; }     return dependenciesSatisfied; } }

//The actual job object. It is mostly a data wrapper, and holds no real functionality. Twirl.Bits.Jobs.Job = function(name, func, funcParas, settings) { if (!(name && func)) throw "Name or function of new job is undefined";

this.Name = name; this.Func = func; this.FuncParas = funcParas || []; this.Settings = jQuery.extend({waitfor:[], nosideeffects:false, retries:1}, settings || {}); //base settings. Immutable. this.Retries = 0; //the current retries counter this.Status = "waiting"; //waiting, running, success, failure }

//This job finished successfully Twirl.Bits.Jobs.Job.prototype.Success = function(result) { Twirl.Bits.Jobs.Success(this, result); }

//This job failed. Depending on its settings, it might retry. //if fatal is true, an unrecoverable error occured. If the jobs //onfailrestart property is set, it will attempt to backtrack a //number of times Twirl.Bits.Jobs.Job.prototype.Fail = function(info, fatal) { Twirl.Bits.Jobs.Fail(this, info, fatal); }

Twirl.Bits.Jobs.Job.prototype.Fatal = function(info) { Twirl.Bits.Jobs.Fail(this, info, true); }

Twirl.Bits.Jobs.Job.prototype.Start = function { Twirl.Bits.Jobs.__start(this); }

Twirl.Bits.Jobs.Job.prototype.Reinit = function { this.Status = "waiting"; delete this.Result; delete this.BacktrackingWaitList; delete this.BacktrackingWaitCount; delete this.BacktrackingJob; }

Twirl.Bits.Jobs.Job.prototype.Info = function { //TODO: log an info message Twirl.Bits.Log.apply(Twirl.Bits, arguments); } Twirl.Bits.Jobs.Job.prototype.Log = function { //TODO: log a log message Twirl.Bits.Log.apply(Twirl.Bits, arguments); }

Twirl.Bits.Jobs.JobStub = function(name) { this.Name = name; }

Twirl.Bits.Jobs.Common = { //get revision data of the given page //If the page is a redirect, the object will have a "redirect" key GetPageData : function(page) {     var job = this; var parameters = { prop:['info','revisions'], rvprop:['content','ids', 'timestamp'], inprop:['redirect','pageid','title'], intoken:'edit', };

//accept both page id and full page name if (typeof page == 'string') parameters.titles = page; else if (typeof page == 'number') parameters.pageids = page; else { job.Fatal('Invalid parameter passed to Twirl.Bits.Jobs.GetPage: "$1"', page ); return; }

job.Log('Retrieving last revision of "$1"', page)

Twirl.Bits.Api({       parameters:parameters,        onSuccess:function(result)        {          job.Info('Recieved API revision data of "$1"', page);          try          {            for (var pageId in result.query.pages)            {              if (pageId=='_element') continue;              var revisionData = result.query.pages[pageId];              job.Success({ pageid: revisionData.pageid, title: revisionData.title, redirect: revisionData.redirect=="", starttimestamp: revisionData.starttimestamp, edittoken: revisionData.edittoken, revid: revisionData.revisions['0'].revid, basetimestamp: revisionData.revisions['0'].timestamp, text: revisionData.revisions['0']['*'], });             return;            }          }          catch(e) { job.Fatal(e); } //we got an API response, but the result couldn't be parsed? No need to retry, it's fatal.        },        onHttpErrpr:function(result){ job.Fail(result, false); },        onApiErrpr:function(result){ job.Fail(result, true); },      }); }, //Get the raw page source of a page. Does not use the API, and can return a cached result. GetRaw : function(page) {     Twirl.Bits.Jobs.Common.GetUri.call(this, mw.config.get('wgServer') + mw.config.get('wgScriptPath') + "/index.php?action=raw&ctype=text/javascript&title="+encodeURIComponent(page)); }, //Get the raw page source of a page. Does not use the API, and can return a cached result. GetUri : function(uri) {     var job = this; var __onComplete = function(reqWrapper, httpResult) {	     if (reqWrapper.finished) return; reqWrapper.finished = true; if (httpResult!=0 && (httpResult<200 || httpResult>=300)) { Twirl.Bits.Error("HTTP error $1", httpResult); return; } reqWrapper.req.onreadystatechange = Twirl.Bits.Void; job.Success({	       text:reqWrapper.req.responseText	      }); };     var req = sajax_init_object; req.open('GET', uri, true); req.setRequestHeader('Accept', 'text/javascript, text/html, application/xml, text/xml, *'+'/*'); var reqWrapper = {req:req, finished:false}; setTimeout(function { __onComplete(reqWrapper, 418); }, 10000); //manually timeout every request after 10 seconds, teapot style. req.onreadystatechange = function {       //complete? if (reqWrapper.req.readyState != 4) return; __onComplete(reqWrapper, reqWrapper.req.status); };     req.send; }, Edit : function(pageData, minor, watchlist, summary) {     var job = this; }, }

Twirl.Bits.Config = {	__defaultConfig : ( function 	{	 var defaultConfig = {  	  Twirl:  	  {    		SummaryAd : " (TWIRL)",    		Watchlist : "preferences",    		NamespaceId :    		{           Main:0, Article:0,          Talk:1,          User:2,          User_talk:3,          Wikipedia:4, Project:4,          Wikipedia_talk:5, Project_talk:5,          File:6, Image:6,          File_talk:7, Image_talk:7,          MediaWiki:8,          MediaWiki_talk:9,          Template:10,          Template_talk:11,          Help:12,          Help_talk:13,          Category:14,          Category_talk:15,          Thread:90,          Thread_talk:91,          Summary:92,          Summary_talk:93,          Portal:100,          Portal_talk:101,          Book:108,          Book_talk:109,          Special:-1,          Media:-2        }    	    	},    };    switch (skin)    {    	case 'vector':    	  defaultConfig.Twirl.PortletArea = 'right-navigation'; defaultConfig.Twirl.PortletId  = 'p-twirl'; defaultConfig.Twirl.PortletName = 'Twirl'; defaultConfig.Twirl.PortletType = 'menu'; defaultConfig.Twirl.PortletNext = 'p-search'; break; default: defaultConfig.Twirl.PortletId  = 'p-cactions'; break; };   return defaultConfig; }),	__userConfig : {},	__processUserConfigJob : function(result)	{	 if (window.TwirlDebug && window.TwirlDebug.UserConfig) result = {text:window.TwirlDebug.UserConfig};		//as it just so happens, our configuration can just be evaluated to build a proper javascript object:    try    {      if (result.text!="" && result.text!="/* Empty */") Twirl.Bits.Config.__userConfig = eval('('+ result.text +')'); //TODO: Use parseJSON, as soon as bits.wikimedia updates!			Twirl.Bits.Config.__recalculateConfig;    }    catch(e)    {      jsMsg("Error parsing Twirl config: "+e);    }				this.Success;	},	Defaults : function(gadgetName, gadgetData)	{		Twirl.Bits.Config.__defaultConfig[gadgetName] = jQuery.extend(true, {}, gadgetData );		Twirl.Bits.Config.__recalculateConfig;	},	//combines default config and user config into one structure.	__recalculateConfig : function	{		Twirl.Config = jQuery.extend(true, {}, Twirl.Bits.Config.__defaultConfig, Twirl.Bits.Config.__userConfig) }, };

// Twirl gadgets are normally loaded through the configuration, so any code // they execute can rely on Twirl.Bits being loaded. Nonetheless, it is // recommended that all scripts initialize by using //  if (!(window.Twirl && window.Twirl.Bits)) importScript("User:Amalthea/Twirl.js"); //  window.TwirlInit = (window.TwirlInit || []).concat( someInitializationFunction ); // for maximal robustness. Twirl.Bits.__runInitializersJob = function {	var funcs = window.TwirlInit; window.TwirlInit = { concat : function(i,func){ func; } }; Twirl.Bits.Log(funcs); if (funcs) jQuery.each(funcs, window.TwirlInit.concat); this.Success; }

// On page load, load configuration, execute pending gadget initializations, // which also redefines the gadget initialization registration to immediate // execution from now on. $(function { Twirl.Bits.Jobs.Add("Twirl.LoadUserConfig", Twirl.Bits.Jobs.Common.GetRaw, ["User:"+mw.config.get('wgUserName')+"/Twirl.css"]);  Twirl.Bits.Jobs.Add("Twirl.ProcessUserConfig", Twirl.Bits.Config.__processUserConfigJob, [Twirl.Bits.JobResult("Twirl.LoadUserConfig")]);  Twirl.Bits.Jobs.Add("Twirl.RunInitializers", Twirl.Bits.__runInitializersJob, [], { waitfor: ["Twirl.ProcessUserConfig"] }); });

});