User:Lupin/scripter.js
From Wikipedia, the free encyclopedia
Note: After saving, you have to bypass your browser's cache to see the changes. Firefox/Mozilla/Safari: hold down Shift while clicking Reload (or press Ctrl-Shift-R), Internet Explorer: press Ctrl-F5, Opera/Konqueror: press F5.
//~ 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: ///