Jump to content
Wikipedia The Free Encyclopedia

User:Andrybak/Scripts/Unsigned helper.js

From Wikipedia, the free encyclopedia
Code that you insert on this page could contain malicious content capable of compromising your account. If you import a script from another page with "importScript", "mw.loader.load", "iusc", or "lusc", take note that this causes you to dynamically load a remote script, which could be changed by others. Editors are responsible for all edits and actions they perform, including by scripts. User scripts are not centrally supported and may malfunction or become inoperable due to software changes. A guide to help you find broken scripts is available. If you are unsure whether code you are adding to this page is safe, you can ask at the appropriate village pump.
This code will be executed when previewing this page.
This user script seems to have a documentation page at User:Andrybak/Scripts/Unsigned helper.
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.
 /*
  * This is a fork of https://en.wikipedia.org/w/index.php?title=User:Anomie/unsignedhelper.js&oldid=1219219971
  */
 (function(){
 constLOG_PREFIX=`[Unsigned Helper]:`;

 functionerror(...toLog){
 console.error(LOG_PREFIX,...toLog);
 }

 functionwarn(...toLog){
 console.warn(LOG_PREFIX,...toLog);
 }

 functioninfo(...toLog){
 console.info(LOG_PREFIX,...toLog);
 }

 functiondebug(...toLog){
 console.debug(LOG_PREFIX,...toLog);
 }

 constmonths=['January','February','March','April','May','June','July','August','September','October','November','December'];

 constCONFIG={
 undated:'Undated',// [[Template:Undated]]
 unsignedLoggedIn:'Unsigned',// [[Template:Unsigned]]
 unsignedIp:'Unsigned IP',// [[Template:Unsigned IP]]
 };

 if(mw.config.get('wgAction')!=='edit'&&mw.config.get('wgAction')!=='submit'&&document.getElementById("editform")==null){
 info('Not editing a page. Aborting.');
 return;
 }

 info('Loading...');

 functionformatErrorSpan(errorMessage){
 return`<span style="color:maroon;"><b>Error:</b> ${errorMessage}</span>`;
 }

 constLAZY_REVISION_LOADING_INTERVAL=50;

 /**
 	 * Lazily loads revision IDs for a page.
 	 * Gives zero-indexed access to the revisions. Zeroth revision is the newest revision.
 	 */
 classLazyRevisionIdsLoader{
 #pagename;
 #indexedRevisionPromises=[];
 /**
 		 * We are loading revision IDs per LAZY_REVISION_LOADING_INTERVAL
 		 * Each of requests gives us LAZY_REVISION_LOADING_INTERVAL revision IDs.
 		 */
 #historyIntervalPromises=[];
 #api=newmw.Api();

 constructor(pagename){
 this.#pagename=pagename;
 }

 #getLastLoadedInterval(upToIndex){
 leti=0;
 while(this.#historyIntervalPromises[i]!=undefined&&i<=upToIndex){
 i++;
 }
 return[i,this.#historyIntervalPromises[i-1]];
 }

 #createIntervalFromResponse(response){
 if('missing'inresponse.query.pages[0]){
 returnundefined;
 }
 return{
 rvcontinue:response.continue?.rvcontinue,
 revisions:response.query.pages[0].revisions,
 };
 }

 async#loadIntervalsRecursive(index,upToIndex,rvcontinue){
 returnnewPromise(async(resolve,reject)=>{
 // reference documentation: https://en.wikipedia.org/w/api.php?action=help&modules=query%2Brevisions
 constintervalQuery={
 action:'query',
 prop:'revisions',
 rvlimit:LAZY_REVISION_LOADING_INTERVAL,
 rvprop:'ids|user',// no 'content' here; 'user' is just for debugging purposes
 rvslots:'main',
 formatversion:2,// v2 has nicer field names in responses

 titles:this.#pagename,
 };
 if(rvcontinue){
 intervalQuery.rvcontinue=rvcontinue;
 }
 debug('loadIntervalsRecursive Q: index =',index,'upToIndex =',upToIndex,'intervalQuery =',intervalQuery);
 this.#api.get(intervalQuery).then(async(response)=>{
 try{
 // debug('loadIntervalsRecursive R:', response);
 constinterval=this.#createIntervalFromResponse(response);
 this.#historyIntervalPromises[index]=Promise.resolve(interval);
 if(index==upToIndex){
 // we've hit the limit of what we want to load so far
 resolve(interval);
 return;
 }
 if(response.batchcomplete){
 for(leti=index;i<=upToIndex;i++){
 this.#historyIntervalPromises[i]=Promise.resolve(undefined);
 }
 // we've asked for an interval of history which doesn't exist
 resolve(undefined);
 return;
 }
 // recursive call for one more interval
 constignored=awaitthis.#loadIntervalsRecursive(index+1,upToIndex,interval.rvcontinue);
 if(this.#historyIntervalPromises[upToIndex]==undefined){
 resolve(undefined);
 return;
 }
 this.#historyIntervalPromises[upToIndex].then(
 result=>resolve(result),
 rejection=>reject(rejection)
 );
 }catch(e){
 reject('loadIntervalsRecursive: '+e);
 }
 },rejection=>{
 reject('loadIntervalsRecursive via api: '+rejection);
 });
 });
 }

 async#loadInterval(intervalIndex){
 const[firstNotLoadedIntervalIndex,latestLoadedInterval]=this.#getLastLoadedInterval(intervalIndex);
 if(firstNotLoadedIntervalIndex>intervalIndex){
 returnthis.#historyIntervalPromises[intervalIndex];
 }
 constrvcontinue=latestLoadedInterval?.rvcontinue;
 returnthis.#loadIntervalsRecursive(firstNotLoadedIntervalIndex,intervalIndex,rvcontinue);
 }

 #indexToIntervalIndex(index){
 returnMath.floor(index/LAZY_REVISION_LOADING_INTERVAL);
 }

 #indexToIndexInInterval(index){
 returnindex%LAZY_REVISION_LOADING_INTERVAL;
 }

 /**
 		 * @param index zero-based index of a revision to load
 		 */
 asyncloadRevision(index){
 if(this.#indexedRevisionPromises[index]){
 returnthis.#indexedRevisionPromises[index];
 }
 constpromise=newPromise(async(resolve,reject)=>{
 constintervalIndex=this.#indexToIntervalIndex(index);
 try{
 constinterval=awaitthis.#loadInterval(intervalIndex);
 if(interval==undefined){
 resolve(undefined);
 return;
 }
 consttheRevision=interval.revisions[this.#indexToIndexInInterval(index)];
 debug('loadRevision: loaded revision',index,theRevision);
 resolve(theRevision);
 }catch(e){
 reject('loadRevision: '+e);
 }
 });
 this.#indexedRevisionPromises[index]=promise;
 returnpromise;
 }
 }

 /**
 	 * Lazily loads full revisions (wikitext, user, revid, tags, edit summary, etc) for a page.
 	 * Gives zero-indexed access to the revisions. Zeroth revision is the newest revision.
 	 */
 classLazyFullRevisionsLoader{
 #pagename;
 #revisionsLoader;
 #indexedContentPromises=[];
 #api=newmw.Api();

 constructor(pagename){
 this.#pagename=pagename;
 this.#revisionsLoader=newLazyRevisionIdsLoader(pagename);
 }

 /**
 		 * Returns a {@link Promise} with full revision for given index.
 		 */
 asyncloadContent(index){
 if(this.#indexedContentPromises[index]){
 returnthis.#indexedContentPromises[index];
 }
 constpromise=newPromise(async(resolve,reject)=>{
 try{
 constrevision=awaitthis.#revisionsLoader.loadRevision(index);
 if(revision==undefined){
 // this revision doesn't seem to exist
 resolve(undefined);
 return;
 }
 // reference documentation: https://en.wikipedia.org/w/api.php?action=help&modules=query%2Brevisions
 constcontentQuery={
 action:'query',
 prop:'revisions',
 rvlimit:1,// load the big wikitext only for the revision
 rvprop:'ids|user|timestamp|tags|parsedcomment|content',
 rvslots:'main',
 formatversion:2,// v2 has nicer field names in responses

 titles:this.#pagename,
 rvstartid:revision.revid,
 };
 debug('loadContent: contentQuery = ',contentQuery);
 this.#api.get(contentQuery).then(response=>{
 try{
 consttheRevision=response.query.pages[0].revisions[0];
 resolve(theRevision);
 }catch(e){
 // just in case the chain `response.query.pages[0].revisions[0]`
 // is broken somehow
 error('loadContent:',e);
 reject('loadContent:'+e);
 }
 },rejection=>{
 reject('loadContent via api:'+rejection);
 });
 }catch(e){
 error('loadContent:',e);
 reject('loadContent: '+e);
 }
 });
 this.#indexedContentPromises[index]=promise;
 returnpromise;
 }

 asyncloadRevisionId(index){
 returnthis.#revisionsLoader.loadRevision(index);
 }
 }

 functionmidPoint(lower,upper){
 returnMath.floor(lower+(upper-lower)/2);
 }

 /**
 	 * Based on https://en.wikipedia.org/wiki/Module:Exponential_search
 	 */
 asyncfunctionexponentialSearch(lower,upper,candidateIndex,testFunc){
 if(upper===null&&lower===candidateIndex){
 thrownewError(`Wrong arguments for exponentialSearch (${lower}, ${upper}, ${candidateIndex}).`);
 }
 if(lower===upper&&lower===candidateIndex){
 thrownewError("Cannot find it");
 }
 constprogressMessage=`Examining [${lower}, ${upper?upper:'...'}]. Current candidate: ${candidateIndex}`;
 if(awaittestFunc(candidateIndex,progressMessage)){
 if(candidateIndex+1==upper){
 returncandidateIndex;
 }
 lower=candidateIndex;
 if(upper){
 candidateIndex=midPoint(lower,upper);
 }else{
 candidateIndex=candidateIndex*2;
 }
 returnexponentialSearch(lower,upper,candidateIndex,testFunc);
 }else{
 upper=candidateIndex;
 candidateIndex=midPoint(lower,upper);
 returnexponentialSearch(lower,upper,candidateIndex,testFunc);
 }
 }

 classPageHistoryContentSearcher{
 #pagename;
 #contentLoader;
 #progressCallback;

 constructor(pagename,progressCallback){
 this.#pagename=pagename;
 this.#contentLoader=newLazyFullRevisionsLoader(this.#pagename);
 this.#progressCallback=progressCallback;
 }

 setProgressCallback(progressCallback){
 this.#progressCallback=progressCallback;
 }

 async#findMaxIndex(){
 returnexponentialSearch(0,null,1,async(candidateIndex,progressInfo)=>{
 this.#progressCallback(progressInfo+' (max search)');
 constcandidateRevision=awaitthis.#contentLoader.loadRevisionId(candidateIndex);
 if(candidateRevision==undefined){
 returnfalse;
 }
 returntrue;
 });
 }

 asyncfindRevisionWhenTextAdded(text,startIndex){
 info('findRevisionWhenTextAdded: searching for',text);
 returnnewPromise(async(resolve,reject)=>{
 try{
 conststartRevision=awaitthis.#contentLoader.loadRevisionId(startIndex);
 if(startRevision==undefined){
 if(startIndex===0){
 reject("Cannot find the latest revision. Does this page exist?");
 }else{
 reject(`Cannot find the start revision (index=${startIndex}).`);
 }
 return;
 }
 if(startIndex===0){
 constlatestFullRevision=awaitthis.#contentLoader.loadContent(startIndex);
 if(!latestFullRevision.slots.main.content.includes(text)){
 reject("Cannot find text in the latest revision. Did you edit it?");
 return;
 }
 }
 constmaxIndex=(startIndex===0)?null:(awaitthis.#findMaxIndex());
 constfoundIndex=awaitexponentialSearch(startIndex,maxIndex,startIndex+10,async(candidateIndex,progressInfo)=>{
 try{
 this.#progressCallback(progressInfo);
 constcandidateFullRevision=awaitthis.#contentLoader.loadContent(candidateIndex);
 if(candidateFullRevision?.slots?.main?.content==undefined){
 returnundefined;
 }
 // debug('testFunc: checking text of revision:', candidateFullRevision, candidateFullRevision?.slots, candidateFullRevision?.slots?.main);
 returncandidateFullRevision.slots.main.content.includes(text);
 }catch(e){
 reject('testFunc: '+e);
 }
 });
 if(foundIndex===undefined){
 reject("Cannot find this text.");
 return;
 }
 constfoundFullRevision=awaitthis.#contentLoader.loadContent(foundIndex);
 resolve({
 fullRevision:foundFullRevision,
 index:foundIndex,
 });
 }catch(e){
 reject(e);
 }
 });
 }
 }

 functionisRevisionARevert(fullRevision){
 if(fullRevision.tags.includes('mw-rollback')){
 returntrue;
 }
 if(fullRevision.tags.includes('mw-undo')){
 returntrue;
 }
 if(fullRevision.parsedcomment.includes('Undid')){
 returntrue;
 }
 if(fullRevision.parsedcomment.includes('Reverted')){
 returntrue;
 }
 returnfalse;
 }

 functionchooseUnsignedTemplateFromRevision(fullRevision){
 if(typeof(fullRevision.anon)!=='undefined'){
 returnCONFIG.unsignedIp;
 }elseif(typeof(fullRevision.temp)!=='undefined'){
 // Seems unlikely "temporary" users will have a user page, so this seems the better template for them for now.
 returnCONFIG.unsignedIp;
 }else{
 returnCONFIG.unsignedLoggedIn;
 }
 }

 functionchooseTemplate(selectedText,fullRevision){
 constuser=fullRevision.user;
 if(selectedText.includes(`[[User talk:${user}|`)){
 /*
 			 * assume that presense of something that looks like a wikilink to the user's talk page
 			 * means that the message is just undated, not unsigned
 			 * NB: IP editors have `Special:Contributions` and `User talk` in their signature.
 			 */
 returnCONFIG.undated;
 }
 if(selectedText.includes(`[[User:${user}|`)){
 // some ancient undated signatures have only `[[User:` links
 returnCONFIG.undated;
 }
 returnchooseUnsignedTemplateFromRevision(fullRevision);
 }

 functioncreateTimestampWikitext(timestamp){
 /*
 		 * Format is from https://www.mediawiki.org/wiki/Help:Extension:ParserFunctions##time_format_like_in_signatures
 		 *
 		 * The unicode escapes are needed to avoid actual substitution, see
 		 * https://en.wikipedia.org/w/index.php?title=User:Andrybak/Scripts/Unsigned_generator.js&diff=prev&oldid=1229098580
 		 */
 return`\u007B\u007Bsubst:#time:H:i, j xg Y "(UTC)"|${timestamp}}}`;
 }

 functionmakeTemplate(user,timestamp,template){
 // <nowiki>
 constformattedTimestamp=createTimestampWikitext(timestamp);
 if(template==CONFIG.undated){
 return'{{subst:'+template+'|'+formattedTimestamp+'}}';
 }
 return'{{subst:'+template+'|'+user+'|'+formattedTimestamp+'}}';
 // </nowiki>
 }

 functionconstructAd(){
 return" (using [[w:User:Andrybak/Scripts/Unsigned helper|Unsigned helper]])";
 }

 functionappendToEditSummary(newSummary){
 consteditSummaryField=$("#wpSummary:first");
 if(editSummaryField.length==0){
 warn('Cannot find edit summary text field.');
 return;
 }
 // get text without trailing whitespace
 letoldText=editSummaryField.val().trimEnd();
 constad=constructAd();
 if(oldText.includes(ad)){
 oldText=oldText.replace(ad,'');
 }
 letnewText="";
 if(oldText.match(/[*]\/$/)){
 // check if "/* section name */" is present
 newText=oldText+" "+newSummary;
 }elseif(oldText.length!=0){
 newText=oldText+", "+newSummary;
 }else{
 newText=newSummary;
 }
 editSummaryField.val(newText+ad);
 }

 // kept outside of doAddUnsignedTemplate() to keep all the caches
 letsearcher;
 functiongetSearcher(){
 if(searcher){
 returnsearcher;
 }
 constpagename=mw.config.get('wgPageName');
 searcher=newPageHistoryContentSearcher(pagename,progressInfo=>{
 info('Default progress callback',progressInfo);
 });
 returnsearcher;
 }

 asyncfunctiondoAddUnsignedTemplate(){
 constform=document.getElementById('editform');
 constwikitextEditor=form.elements.wpTextbox1;
 letpos=$(wikitextEditor).textSelection('getCaretPosition',{startAndEnd:true});
 lettxt;
 if(pos[0]!=pos[1]){
 txt=wikitextEditor.value.substring(pos[0],pos[1]);
 pos=pos[1];
 }else{
 pos=pos[1];
 if(pos<=0){
 pos=wikitextEditor.value.length;
 }
 txt=wikitextEditor.value.substr(0,pos);
 txt=txt.replace(newRegExp('[\\s\\S]*\\d\\d:\\d\\d, \\d+ ('+months.join('|')+') \\d\\d\\d\\d \\(UTC\\)'),'');
 txt=txt.replace(/[\s\S]*\n=+.*=+\s*\n/,'');
 }
 txt=txt.replace(/^\s+|\s+$/g,'');

 // TODO maybe migrate to https://www.mediawiki.org/wiki/OOUI/Windows/Message_Dialogs
 constmainDialog=$('<div>Examining...</div>').dialog({
 buttons:{
 Cancel:function(){
 mainDialog.dialog('close');
 }
 },
 modal:true,
 title:'Adding {{unsigned}}'
 });

 getSearcher().setProgressCallback(debugInfo=>{
 /* progressCallback */
 info('Showing to user:',debugInfo);
 mainDialog.html(debugInfo);
 });

 functionapplySearcherResult(searcherResult){
 constfullRevision=searcherResult.fullRevision;
 consttemplate=chooseTemplate(txt,fullRevision);
 consttemplateWikitext=makeTemplate(
 fullRevision.user,
 fullRevision.timestamp,
 template
 );
 // https://doc.wikimedia.org/mediawiki-core/master/js/module-jquery.textSelection.html
 $(wikitextEditor).textSelection(
 'encapsulateSelection',{
 post:" "+templateWikitext
 }
 );
 appendToEditSummary(`mark [[Template:${template}|{{${template}}}]] [[Special:Diff/${fullRevision.revid}]]`);
 mainDialog.dialog('close');
 }

 functionreportSearcherResultToUser(searcherResult,dialogTitle,useCb,keepLookingCb,cancelCb,createMainMessageDivFn){
 constfullRevision=searcherResult.fullRevision;
 constrevid=fullRevision.revid;
 constcomment=fullRevision.parsedcomment;
 constquestionDialog=createMainMessageDivFn()
 .dialog({
 title:dialogTitle,
 modal:true,
 buttons:{
 "Use that revision":function(){
 questionDialog.dialog('close');
 useCb();
 },
 "Keep looking":function(){
 questionDialog.dialog('close');
 keepLookingCb();
 },
 "Cancel":function(){
 questionDialog.dialog('close');
 cancelCb();
 },
 }
 });
 }

 functionreportPossibleRevertToUser(searcherResult,useCb,keepLookingCb,cancelCb){
 constfullRevision=searcherResult.fullRevision;
 constrevid=fullRevision.revid;
 constcomment=fullRevision.parsedcomment;
 reportSearcherResultToUser(searcherResult,"Possible revert!",useCb,keepLookingCb,cancelCb,()=>{
 return$('<div>').append(
 "The ",
 $('<a>').prop({
 href:'/w/index.php?diff=prev&oldid='+revid,
 target:'_blank'
 }).text(`found revision (index=${searcherResult.index})`),
 " may be a revert: ",
 comment
 );
 });
 }

 functionreportNormalSearcherResultToUser(searcherResult,useCb,keepLookingCb,cancelCb){
 constfullRevision=searcherResult.fullRevision;
 constrevid=fullRevision.revid;
 constcomment=fullRevision.parsedcomment;
 reportSearcherResultToUser(searcherResult,"Do you want to use this?",useCb,keepLookingCb,cancelCb,()=>{
 return$('<div>').append(
 "Found a revision: ",
 $('<a>').prop({
 href:'/w/index.php?diff=prev&oldid='+revid,
 target:'_blank'
 }).text(`[[Special:Diff/${revid}]] (index=${searcherResult.index})`),
 ".",
 $('<br/>'),
 "Comment: ",
 comment
 );
 });
 }

 functionsearchFromIndex(index){
 searcher.findRevisionWhenTextAdded(txt,index).then(searcherResult=>{
 if(!mainDialog.dialog('isOpen')){
 // user clicked [cancel]
 return;
 }
 info('Searcher found:',searcherResult);
 constuseCallback=()=>{/* use */
 applySearcherResult(searcherResult);
 };
 constkeepLookingCallback=()=>{/* keep looking */
 // recursive call from a differfent index: `+1` is very important here
 searchFromIndex(searcherResult.index+1);
 };
 constcancelCallback=()=>{/* cancel */
 mainDialog.dialog('close');
 };
 if(isRevisionARevert(searcherResult.fullRevision)){
 reportPossibleRevertToUser(searcherResult,useCallback,keepLookingCallback,cancelCallback);
 return;
 }
 reportNormalSearcherResultToUser(searcherResult,useCallback,keepLookingCallback,cancelCallback);
 },rejection=>{
 error(`Searcher cannot find requested index=${index}. Got error:`,rejection);
 if(!mainDialog.dialog('isOpen')){
 // user clicked [cancel]
 return;
 }
 mainDialog.html(formatErrorSpan(`${rejection}`));
 });
 }

 searchFromIndex(0);
 }

 window.unsignedHelperAddUnsignedTemplate=function(event){
 mw.loader.using(['mediawiki.util','jquery.ui'],doAddUnsignedTemplate);
 event.preventDefault();
 event.stopPropagation();
 returnfalse;
 }

 if(!window.charinsertCustom){
 window.charinsertCustom={};
 }
 if(!window.charinsertCustom.Insert){
 window.charinsertCustom.Insert='';
 }
 window.charinsertCustom.Insert+=' {{unsigned}}\x10unsignedHelperAddUnsignedTemplate';
 if(!window.charinsertCustom['Wiki markup']){
 window.charinsertCustom['Wiki markup']='';
 }
 window.charinsertCustom['Wiki markup']+=' {{unsigned}}\x10unsignedHelperAddUnsignedTemplate';
 if(window.updateEditTools){
 window.updateEditTools();
 }
 })();

AltStyle によって変換されたページ (->オリジナル) /