Jump to content
Wikipedia The Free Encyclopedia

User:Polygnotus/Scripts/DraftCategories.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.
Documentation for this user script can be added at User:Polygnotus/Scripts/DraftCategories.
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.
 // Script to remove {{Draft categories}} template while preserving categories
 //https://en.wikipedia.org/wiki/Category:Articles_using_draft_categories

 //According to https://en.wikipedia.org/wiki/Wikipedia:Categorization#Categorizing_draft_pages
 // there are 4 methods: nowiki, html comment, [[:Category and {{Draft categories}}

 //Currently https://en.wikipedia.org/wiki/Wikipedia:Categorization#Categorizing_draft_pages doesn't even mention Tolbot and lists 2 scripts that could be merged into one 
 //the scripts listed dont even support all 4 methods, dont support all redirects to the template (if draft then cat, if mainspace uncat)

 //deduplicate then insert at MOS:ORDER

 //Can't find Tolbot code, not sure how well it does

 //How do I find [[:Category in mainspace? Ideally without parsing the dump?

 // Searching for insource:/\[\[:Category:/ returns 20k articles...

 //i don't see a restriction on linking to another namespace in the MOS, although it violates least astonishment
 //api returns only 10k results oof. 
 //check if [[:Category is near the bottom

 //https://en.wikipedia.org/wiki/User:DannyS712/Draft_no_cat
 //https://en.wikipedia.org/wiki/Category:AfC_submissions_with_categories
 //https://en.wikipedia.org/wiki/Category:AfC_submissions_with_categories


 (function(){
 'use strict';

 // ============ CONFIGURATION ============
 constTESTING_MODE=false;// Set to false to actually edit pages
 constMAX_ARTICLES=null;// Maximum number of articles to process (set to null for unlimited)
 constDELAY_BETWEEN_REQUESTS=5000;// milliseconds between API calls (5 seconds)
 constDELAY_BETWEEN_EDITS=5000;// milliseconds between actual edits (10 seconds)
 constMAX_RETRIES=3;
 constBATCH_SIZE=50;// Number of pages to fetch at once

 // ============ MAIN EXECUTION ============

 // Add a link to the toolbox to run the script
 if(mw.config.get('wgCanonicalSpecialPageName')===false){
 mw.loader.using(['mediawiki.util'],function(){
 mw.util.addPortletLink(
 'p-tb',
 '#',
 'Remove Draft Categories',
 't-remove-draft-cats',
 'Remove Draft categories template from articles'
 );
 $('#t-remove-draft-cats').click(function(e){
 e.preventDefault();
 if(confirm('This will process all articles in Category:Articles using draft categories. Continue?')){
 processDraftCategories();
 }
 });
 });
 }

 // ============ API FUNCTIONS ============

 functionsleep(ms){
 returnnewPromise(resolve=>setTimeout(resolve,ms));
 }

 asyncfunctiongetCategoryMembers(category,cmcontinue){
 constapi=newmw.Api({
 ajax:{
 headers:{
 'Api-User-Agent':'DraftCategoriesRemover/1.0 (User:'+mw.config.get('wgUserName')+')'
 }
 }
 });

 constparams={
 action:'query',
 list:'categorymembers',
 cmtitle:category,
 cmlimit:BATCH_SIZE,
 cmnamespace:0,// Main namespace only
 format:'json'
 };

 if(cmcontinue){
 params.cmcontinue=cmcontinue;
 }

 letretries=0;
 while(retries<MAX_RETRIES){
 try{
 constresult=awaitapi.get(params);
 returnresult;
 }catch(error){
 retries++;
 console.error(`API error (attempt ${retries}/${MAX_RETRIES}):`,error);
 if(retries>=MAX_RETRIES){
 throwerror;
 }
 awaitsleep(DELAY_BETWEEN_REQUESTS*retries);
 }
 }
 }

 asyncfunctiongetPageContent(title){
 constapi=newmw.Api({
 ajax:{
 headers:{
 'Api-User-Agent':'DraftCategoriesRemover/1.0 (User:'+mw.config.get('wgUserName')+')'
 }
 }
 });

 letretries=0;
 while(retries<MAX_RETRIES){
 try{
 constresult=awaitapi.get({
 action:'query',
 titles:title,
 prop:'revisions',
 rvprop:'content|timestamp',
 rvslots:'main',
 format:'json'
 });

 constpages=result.query.pages;
 constpageId=Object.keys(pages)[0];
 constpage=pages[pageId];

 if(page.revisions&&page.revisions[0]){
 return{
 content:page.revisions[0].slots.main['*'],
 timestamp:page.revisions[0].timestamp
 };
 }
 returnnull;
 }catch(error){
 retries++;
 console.error(`API error fetching ${title} (attempt ${retries}/${MAX_RETRIES}):`,error);
 if(retries>=MAX_RETRIES){
 throwerror;
 }
 awaitsleep(DELAY_BETWEEN_REQUESTS*retries);
 }
 }
 }

 asyncfunctioneditPage(title,newContent,timestamp,summary){
 if(TESTING_MODE){
 console.log(`[TESTING MODE] Would edit ${title} with summary: ${summary}`);
 return{success:true,testing:true};
 }

 constapi=newmw.Api({
 ajax:{
 headers:{
 'Api-User-Agent':'DraftCategoriesRemover/1.0 (User:'+mw.config.get('wgUserName')+')'
 }
 }
 });

 letretries=0;
 while(retries<MAX_RETRIES){
 try{
 constresult=awaitapi.postWithToken('csrf',{
 action:'edit',
 title:title,
 text:newContent,
 basetimestamp:timestamp,
 summary:summary,
 minor:true,
 format:'json'
 });
 returnresult;
 }catch(error){
 retries++;
 console.error(`Edit error for ${title} (attempt ${retries}/${MAX_RETRIES}):`,error);
 if(retries>=MAX_RETRIES){
 throwerror;
 }
 awaitsleep(DELAY_BETWEEN_REQUESTS*retries);
 }
 }
 }

 // ============ PROCESSING FUNCTIONS ============

 functionfindCategoryInsertionPoint(content){
 // Categories go at position 8 in End matter (MOS:ORDER)
 // After: defaultsort, authority control, taxonbar, portal bar, navboxes
 // Before: improve categories, uncategorized, stub templates

 // Look for stub templates (these come AFTER categories)
 conststubMatch=content.match(/\{\{[^}]*stub\}\}/i);
 if(stubMatch){
 returnstubMatch.index;
 }

 // Look for {{Improve categories}} or {{Uncategorized}}
 constimproveCatMatch=content.match(/\{\{\s*(Improve categories|Uncategorized)\s*\}\}/i);
 if(improveCatMatch){
 returnimproveCatMatch.index;
 }

 // Look for existing categories (if any) to insert near them
 constexistingCatMatch=content.match(/\[\[Category:[^\]]+\]\]/i);
 if(existingCatMatch){
 // Find the last category
 constallCats=Array.from(content.matchAll(/\[\[Category:[^\]]+\]\]/gi));
 if(allCats.length>0){
 constlastCat=allCats[allCats.length-1];
 returnlastCat.index+lastCat[0].length;
 }
 }

 // Look for defaultsort (categories come AFTER this)
 constdefaultsortMatch=content.match(/\{\{\s*DEFAULTSORT\s*:[^}]+\}\}/i);
 if(defaultsortMatch){
 returndefaultsortMatch.index+defaultsortMatch[0].length;
 }

 // Look for authority control templates (categories come AFTER these)
 constauthorityMatch=content.match(/\{\{\s*Authority control[^}]*\}\}/i);
 if(authorityMatch){
 returnauthorityMatch.index+authorityMatch[0].length;
 }

 // Look for {{Taxonbar}}
 consttaxonbarMatch=content.match(/\{\{\s*Taxonbar[^}]*\}\}/i);
 if(taxonbarMatch){
 returntaxonbarMatch.index+taxonbarMatch[0].length;
 }

 // Look for {{Portal bar}} or {{Subject bar}}
 constportalBarMatch=content.match(/\{\{\s*(Portal bar|Subject bar)[^}]*\}\}/i);
 if(portalBarMatch){
 returnportalBarMatch.index+portalBarMatch[0].length;
 }

 // Look for navboxes (categories come AFTER these)
 constallNavboxes=Array.from(content.matchAll(/\{\{\s*[Nn]avbox[^}]*\}\}/g));
 if(allNavboxes.length>0){
 constlastNavbox=allNavboxes[allNavboxes.length-1];
 returnlastNavbox.index+lastNavbox[0].length;
 }

 // If nothing found, put at the very end
 returncontent.length;
 }

 functionremoveDraftCategoriesTemplate(content){
 consttemplateNames=[
 'Draft categories',
 'Draft cats',
 'Draftcat',
 'Draft Categories',
 'Draft category',
 'Draft cat',
 'Afc categories',
 'Draftcats'
 ];

 consttemplatePattern=templateNames.map(name=>
 name.replace(/\s/g,'\\s*')
 ).join('|');

 // Find the template start
 conststartRegex=newRegExp(
 '\\{\\{\\s*(?:'+templatePattern+')\\s*\\|',
 'gi'
 );

 letmodified=false;
 letnewContent=content;
 letmatch;

 while((match=startRegex.exec(content))!==null){
 conststartPos=match.index;
 constcontentStart=startPos+match[0].length;

 // Count braces to find the matching closing braces
 letbraceCount=2;// We start with {{
 letpos=contentStart;
 letendPos=-1;

 while(pos<content.length&&braceCount>0){
 if(content.substr(pos,2)==='{{'){
 braceCount+=2;
 pos+=2;
 }elseif(content.substr(pos,2)==='}}'){
 braceCount-=2;
 pos+=2;
 if(braceCount===0){
 endPos=pos-2;// Position of the final }}
 }
 }else{
 pos++;
 }
 }

 if(endPos!==-1){
 // Extract the content between the pipes and the closing braces
 letcategories=content.substring(contentStart,endPos).trim();

 // Fix [[:Category: to [[Category:
 categories=categories.replace(/\[\[:Category:/gi,'[[Category:');

 // Unwrap categories inside HTML comments: <!-- [[Category:...]] -->
 categories=categories.replace(/<!--\s*(\[\[Category:[^\]]+\]\])\s*-->/gi,'1ドル');

 // Unwrap categories inside nowiki tags: <nowiki>[[Category:...]]</nowiki>
 categories=categories.replace(/<nowiki>\s*(\[\[Category:[^\]]+\]\])\s*<\/nowiki>/gi,'1ドル');

 // Now deduplicate all categories
 constcategoryRegex=/\[\[Category:[^\]]+\]\]/gi;
 constfoundCategories=categories.match(categoryRegex)||[];

 // Normalize and deduplicate (case-insensitive)
 constuniqueCategories=[];
 constseen=newSet();

 for(constcatoffoundCategories){
 constnormalized=cat.toLowerCase();
 if(!seen.has(normalized)){
 seen.add(normalized);
 uniqueCategories.push(cat);
 }
 }

 // Reconstruct with unique categories
 constdeduplicatedCategories=uniqueCategories.join('\n');

 if(deduplicatedCategories){
 // Find insertion point based on MOS:ORDER
 constinsertionPoint=findCategoryInsertionPoint(content);

 // Remove the template
 constbeforeTemplate=content.substring(0,startPos);
 constafterTemplate=content.substring(endPos+2);

 // Determine where to insert categories
 if(insertionPoint<=startPos){
 // Insertion point is before the template
 constbeforeInsertion=content.substring(0,insertionPoint).trimEnd();
 constbetweenInsertionAndTemplate=content.substring(insertionPoint,startPos);
 constafterTemplateContent=afterTemplate.trimStart();

 newContent=beforeInsertion+'\n\n'+deduplicatedCategories+'\n'+betweenInsertionAndTemplate.trimStart()+afterTemplateContent;
 }elseif(insertionPoint>endPos+2){
 // Insertion point is after the template
 constadjustedInsertionPoint=insertionPoint-(endPos+2-startPos);
 constbeforeTemplateContent=beforeTemplate.trimEnd();
 constbetweenTemplateAndInsertion=afterTemplate.substring(0,adjustedInsertionPoint).trimEnd();
 constafterInsertion=afterTemplate.substring(adjustedInsertionPoint).trimStart();

 newContent=beforeTemplateContent+betweenTemplateAndInsertion+'\n\n'+deduplicatedCategories+'\n'+afterInsertion;
 }else{
 // Insertion point is within the template area, just replace
 newContent=beforeTemplate.trimEnd()+'\n\n'+deduplicatedCategories+'\n'+afterTemplate.trimStart();
 }

 // Clean up excessive whitespace
 newContent=newContent.replace(/\n{3,}/g,'\n\n').trim()+'\n';

 modified=true;
 }else{
 // No categories found, just remove the template
 newContent=content.substring(0,startPos)+content.substring(endPos+2);
 newContent=newContent.replace(/\n{3,}/g,'\n\n').trim()+'\n';
 modified=true;
 }

 // Update content for next iteration
 content=newContent;
 // Reset regex
 startRegex.lastIndex=0;
 }
 }

 return{
 modified:modified,
 content:newContent
 };
 }

 asyncfunctionprocessPage(title){
 console.log(`Processing: ${title}`);

 try{
 constpageData=awaitgetPageContent(title);
 if(!pageData){
 console.error(`Could not fetch content for ${title}`);
 return{success:false,error:'Could not fetch content'};
 }

 constresult=removeDraftCategoriesTemplate(pageData.content);

 if(!result.modified){
 console.log(`No Draft categories template found in ${title}`);
 return{success:true,skipped:true};
 }

 if(TESTING_MODE){
 console.log(`[TESTING MODE] Would remove Draft categories template from ${title}`);
 console.log('Original excerpt:',pageData.content.substring(0,500));
 console.log('Modified excerpt:',result.content.substring(0,500));
 }else{
 awaiteditPage(
 title,
 result.content,
 pageData.timestamp,
 'Removing [[Template:Draft categories]] wrapper (categories preserved and deduplicated per [[MOS:ORDER]])'
 );
 console.log(`Successfully edited ${title}`);
 // Extra delay after actual edits
 awaitsleep(DELAY_BETWEEN_EDITS);
 }

 return{success:true};
 }catch(error){
 console.error(`Error processing ${title}:`,error);
 return{success:false,error:error};
 }
 }

 asyncfunctionprocessDraftCategories(){
 console.log('=== Draft Categories Removal Script ===');
 console.log('Testing mode:',TESTING_MODE);
 console.log('Max articles to process:',MAX_ARTICLES===null?'unlimited':MAX_ARTICLES);
 console.log('Delay between API reads:',DELAY_BETWEEN_REQUESTS+'ms');
 console.log('Delay between edits:',DELAY_BETWEEN_EDITS+'ms');

 conststats={
 total:0,
 processed:0,
 skipped:0,
 failed:0
 };

 try{
 letcmcontinue=null;

 do{
 console.log('\n--- Fetching batch of pages ---');
 constresult=awaitgetCategoryMembers('Category:Articles using draft categories',cmcontinue);

 if(!result.query||!result.query.categorymembers){
 console.log('No more pages found');
 break;
 }

 constpages=result.query.categorymembers;
 console.log(`Found ${pages.length} pages in this batch`);

 for(constpageofpages){
 // Check if we've hit the limit
 if(MAX_ARTICLES!==null&&stats.total>=MAX_ARTICLES){
 console.log(`\nReached maximum article limit (${MAX_ARTICLES}). Stopping.`);
 break;
 }

 stats.total++;
 constprocessResult=awaitprocessPage(page.title);

 if(processResult.success){
 if(processResult.skipped){
 stats.skipped++;
 }else{
 stats.processed++;
 }
 }else{
 stats.failed++;
 }

 // Use standard delay for read operations
 awaitsleep(DELAY_BETWEEN_REQUESTS);
 }

 // Break outer loop if we hit the limit
 if(MAX_ARTICLES!==null&&stats.total>=MAX_ARTICLES){
 break;
 }

 cmcontinue=result.continue?result.continue.cmcontinue:null;

 }while(cmcontinue);

 console.log('\n=== Processing Complete ===');
 console.log('Total pages checked:',stats.total);
 console.log('Successfully processed:',stats.processed);
 console.log('Skipped (no template found):',stats.skipped);
 console.log('Failed:',stats.failed);

 if(TESTING_MODE){
 console.log('\n*** TESTING MODE WAS ENABLED - NO ACTUAL EDITS WERE MADE ***');
 }

 }catch(error){
 console.error('Fatal error:',error);
 }
 }

 })();

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