//~ 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: ///