Note: After saving, you have to bypass your browser's cache to see the changes. Google Chrome, Firefox, Microsoft Edge and Safari: Hold down the ⇧ Shift key and click the Reload toolbar button. For details and instructions about other browsers, see Wikipedia:Bypass your cache.
//~ 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: ///