Jump to content

User:Lupin/scripter.js

From Wikipedia, the free encyclopedia
The printable version is no longer supported and may have rendering errors. Please update your browser bookmarks and please use the default browser print function instead.
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: ///