User:Lupin/scripter.js

//~ Scripter: the main object here. Inserts code when editing //~ monobook.js (in fact, when editing any page)

//~ construtor function Scripter{ this.startString='// Scripter: managed code begins'; this.startScript='// Scripter: managed script'; this.actions=[]; this.scripts=[]; }

//~ Scripter.prototype.getOrig: grab the content of the file before we //~ mess around with it. Store it in this.orig.

Scripter.prototype.getOrig=function { this.textArea=document.getElementById('wpTextbox1'); if (!this.textArea) return false; this.orig=this.textArea.value; return this.orig; }

//~ Scripter.prototype.parseOrig: take this.orig, and look for special //~ comments inserted by previous Scripter instances. Store data in //~ this.startLine, this.endLine, this.chunk (list of lines we're //~ going to manipulate) and if we find a chunk, call this.parseChunk

Scripter.prototype.parseOrig=function(startAt) { if (!this.orig) return false; var lines=this.orig.split('\n'); for (var i=0; i<lines.length; ++i) { if (lines[i].indexOf(this.startString)==0) { var endString=lines[i].split('begins').join('ends'); for (var j=i; j<lines.length; ++j) { if (lines[j]==endString) { // got it         this.startLine=i; this.endLine=j+1; this.chunk=lines.slice(i,j); this.parseChunk; return true; }     }    }  }  this.startLine=lines.length; this.endLine=lines.length+1; this.chunk=[]; return false; }

//~ Scripter.prototype.parseChunk: look for script objects referred to //~ in the chunk we found in parseOrig. Again, we're looking for //~ special comments on a line-by-line basis. Complain if stuff seems //~ wonky. Store the scripts we find in this.scripts, and put metadata //~ in a new subobject of the script, script.meta Scripter.prototype.parseChunk=function { if (!this.chunk) return false; var lines=this.chunk; var scripts=[]; for (var i=0; i<lines.length; ++i) { if (lines[i].indexOf(this.startScript)==0) { if (scripts.length) scripts[scripts.length-1].meta.chunkEndLine=i-1; // ({..}) force {} to be seen as delimiting an object, not grouping braces var evalMe='('+lines[i].replace(this.startScript, '') + ')'; try { var scriptDesc=eval(evalMe); } catch (err) { alert( 'Bad script description at line '+ this.startLine + i); return false; } scriptDesc.meta={}; scriptDesc.meta.chunkStartLine=i; scripts.push(scriptDesc); } }  if (scripts.length) scripts[scripts.length-1].meta.chunkEndLine=lines.length-2; this.scripts=scripts; return scripts; }

//~ Scripter.prototype.gatherScriptData (script): given a script, we //~ use xmlhttp to download the content of script.src. We set the //~ downloading status of the script in script.meta.status and give //~ success/failure functions to call when the download finishes.

Scripter.prototype.gatherScriptData=function(script) { if (!script.src) return false; var titleBase='http://en.wikipedia.org/w/index.php?action=raw'; var savedThis=this; if (typeof script.meta == 'undefined') script.meta={}; script.meta.status='downloading'; var onComplete=function(req,bundle) { script.meta.content=req.responseText; script.meta.status='complete'; } var onFailure=function(req,bundle) { script.meta.status='failed'; confirm ('One or more downloads failed. Retry?') && this.downloadScripts(true); } var url=titleBase + ( script.oldid ? '&oldid='+script.oldid : '') + '&title='+script.src; scripter_download({url: url, onSuccess: onComplete, onFailure: onFailure}); return true; }

//~ Scripter.prototype.downloadScripts(retry): loop over this.scripts //~ and call gatherScriptData to grab them if appropriate (based on //~ script.meta.status). Return the number of scripts which have not //~ yet completed downloading successfully, or -1 if something goes //~ wrong. Scripter.prototype.downloadScripts=function(retry) { // returns -1 on failure // 0 on all complete // n > 0 if some remain if (!this.scripts) return -1; var incomplete=0; for (var i=0; i<this.scripts.length; ++i) { var script=this.scripts[i]; if (!script) continue; if (typeof script.meta=='undefined') script.meta={}; switch (script.meta.status) { case 'complete': break; case 'failed': incomplete++; if (retry) { this.gatherScriptData(script); }     break; case 'downloading': incomplete++; break; default: incomplete++; this.gatherScriptData(script); } }  return incomplete; }

//~ Scripter.prototype.download(onComplete): run downloadScripts every //~ 0.5 seconds. When it says that all is done, call onComplete Scripter.prototype.download=function(onComplete) { if (this.downloadScripts===0) return onComplete; var savedThis=this; scripter_runOnce(function {savedThis.download.apply(savedThis, [onComplete])}, 500); }

//~ Scripter.prototype.concoctStanza(script): make the bit of the //~ chunk we intend to write corresponding to the script. This takes //~ the form of a special comment, containing all string and integer //~ properties of the script expressed in a form suitable for feeding //~ to eval. Scripter.prototype.concoctStanza=function(script) { var ret=this.startScript; ret += ' {'; var tmp=[]; for (var prop in script) { switch (typeof script[prop]) { case 'string': tmp.push(prop + ':' + '"' + script[prop].split('"').join('\\"') + '"'); break; case 'number': tmp.push(prop + ':' + script[prop]); break; } }  ret += tmp.join(', '); ret += '}\n'; if (script.meta.content) ret += script.meta.content + '\n'; return ret; }

//~ Scripter.prototype.concoctNewchunk: make the new chunk, with //~ special comments at the start and end, and script stanzas from //~ concoctStanta(script) in between. Scripter.prototype.concoctNewchunk=function { var magic=''; do {magic=(new Date).getTime.toString;} while (this.orig.indexOf(magic) != -1); var ret=[this.startString, magic].join(' ') + '\n'; for (var i=0; i<this.scripts.length; ++i) { if (!this.scripts[i]) continue; ret += this.concoctStanza(this.scripts[i]) + '\n'; } ret += [this.startString.split('begins').join('ends'), magic].join(' '); return ret; }

//~ Scripter.prototype.doActions: run over the actions array and carry //~ out the instructions. Look for actions[i].action (can be 'install' //~ or 'remove') and use data actions[i].script to identify the //~ script. We only need provide the actions[i].script.name for //~ removal, but have to give a complete script spec for installation Scripter.prototype.doActions=function { for (var i=0; i< this.actions.length; ++i) { var script=this.actions[i].script; if (this.actions[i].action=='install') { var done=false; for (var j=0; j<this.scripts.length; ++j) { if (!this.scripts[j]) continue; if (this.scripts[j].name==script.name) { // replace old with new this.scripts[j]=script; done=true; }     }      if (!done) this.scripts.push(script); }   else if (this.actions[i].action=='remove') { for (var j=0; j<this.scripts.length; ++j) { if(! this.scripts[j]) continue; if (this.scripts[j].name==script.name) { this.scripts[j]=null; }     }    }  } }

//~ Scripter.prototype.install, Scripter.prototype.finishInstall: run //~ the stuff above in the right order. We need two functions as we //~ wait for the downloads to complete in between. Scripter.prototype.install=function { document.title='Installing...'; this.getOrig; this.parseOrig; this.doActions; var savedThis=this; this.download(function {savedThis.finishInstall.apply(savedThis)}); } Scripter.prototype.finishInstall=function { var newChunk=this.concoctNewchunk; var lines=this.orig.split('\n'); var newLines=lines.slice(0,this.startLine).join('\n')+'\n'; newLines += newChunk+'\n'; newLines+=lines.slice(this.endLine).join('\n'); this.textArea.value=newLines; document.title+=' all done.'; }

//////////////////// // Utility functions //////////////////// function scripter_runOnce(f, time) { var i=scripter_runOnce.timers.length; var ff = function { clearInterval(scripter_runOnce.timers[i]); f }; var timer=setInterval(ff, time); scripter_runOnce.timers.push(timer); } scripter_runOnce.timers=[];

function scripter_download(bundle) { // mandatory: bundle.url, // optional: bundle.onSuccess, bundle.onFailure, bundle.otherStuff var x = window.XMLHttpRequest ? new XMLHttpRequest : window.ActiveXObject ? new ActiveXObject("Microsoft.XMLHTTP") : false; if (!x) return false; x.onreadystatechange=function { x.readyState==4 && scripter_downloadComplete(x,bundle); }; x.open("GET",bundle.url,true); x.send(null); return true; }

function scripter_downloadComplete(x,bundle) { x.status==200 && ( bundle.onSuccess && bundle.onSuccess(x,bundle) || true ) || ( bundle.onFailure && bundle.onFailure(x,bundle) || alert(x.statusText)); }

function WPUS(name) { return 'Wikipedia:WikiProject_User_scripts/Scripts/' + name + '.js'; } function LupinScript(name) { return 'User:Lupin/Scripter/' + name; }

// Testing code starts here

function testScripter { var s=new Scripter; /* s.getOrig; */ /* s.parseOrig; */ /* s.chunk */ /* s.scripts.length */ //s.download(function { alert(s.concoctNewchunk)}) s.actions.push({action:'remove', script:{name:'Navpopups'}}); s.actions.push({action:'install', script:{name: 'addOnloadFunction', src:WPUS('addOnloadFunction'), oldid:25657320}}); s.actions.push({action:'install', script:{name: 'evaluator', src: LupinScript('evaluator'), oldid:30669595}}); s.install }

/* testing chunk // Scripter: managed code begins foobar // Scripter: managed script {name: 'Navpopups', src: 'User:Lupin/Scripter/popups', oldid:30668675} // Scripter: managed script {name: 'add edit section 0', src:'Wikipedia:WikiProject_User_scripts/Scripts/Add_edit_section_0', oldid:21025437} // Scripter: managed script {name: 'LAVT', src:'User:Lupin/Scripter/recent2', oldid:30669328} // Scripter: managed script {name: 'addOnloadFunction', src:'Wikipedia:WikiProject_User_scripts/Scripts/addOnloadFunction.js', oldid:25657320} // Scripter: managed script {name: 'evaluator', src: 'User:Lupin/Scripter/evaluator', oldid:30669595} // Scripter: managed code ends foobar

/// Local Variables: /// /// mode:c /// /// fill-prefix:"//~ " /// /// End: ///