MediaWiki:Gadget-FormattingFixer.js
Note: After publishing, you may have to bypass your browser's cache to see the changes.
- Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
- Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
- Internet Explorer / Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5
- Opera: Press Ctrl-F5.
/**
* FormattingFixer for MediaWiki
*
* Fixes common reference citation issues in wikitext:
* - Reorders refs so definitions come before reuses
* - Standardizes chapter URLs to {{Cite chapter|url=...}} format
* - Detects and helps fix undefined named refs
* - Validates chapter URL format (must have /cXX/pXX)
*
* Works with:
* - VisualEditor (both visual and source modes)
* - WikiEditor (legacy source editor)
*
* Installation:
* 1. Copy this code to MediaWiki:Gadget-FormattingFixer.js
* 2. Add to MediaWiki:Gadgets-definition:
* * FormattingFixer[ResourceLoader|default]|Gadget-FormattingFixer.js
* 3. All users get it by default; can disable in Special:Preferences > Gadgets
*/
(function() {
'use strict';
// Configuration - adjust this pattern if your wiki uses a different URL structure
var config = {
chapterUrlPattern: /https?:\/\/www\.bittersweetcandybowl\.com\/c(\d+(?:\.\d+)?)\/p(\d+)/,
chapterUrlLoose: /https?:\/\/www\.bittersweetcandybowl\.com\/c(\d+(?:\.\d+)?)(\/p\d+)?/,
siteUrlPattern: /https?:\/\/www\.bittersweetcandybowl\.com\//
};
/**
* Parse all references from wikitext
*/
function parseRefs(wikitext) {
var refs = {
definitions: [], // <ref name="X">content</ref>
reuses: [], // <ref name="X" />
anonymous: [] // <ref>content</ref>
};
// Match named refs with content: <ref name="X">content</ref>
var defined_refPattern = /<ref\s+name\s*=\s*["']([^"']+)["']\s*>([\s\S]*?)<\/ref>/gi;
var match;
while ((match = defined_refPattern.exec(wikitext)) !== null) {
refs.definitions.push({
fullMatch: match[0],
name: match[1],
content: match[2],
index: match.index
});
}
// Match named refs without content (reuses): <ref name="X" />
var reusePattern = /<ref\s+name\s*=\s*["']([^"']+)["']\s*\/>/gi;
while ((match = reusePattern.exec(wikitext)) !== null) {
refs.reuses.push({
fullMatch: match[0],
name: match[1],
index: match.index
});
}
// Match anonymous refs: <ref>content</ref>
// Need to be careful not to match named refs
var anonPattern = /<ref\s*>([\s\S]*?)<\/ref>/gi;
while ((match = anonPattern.exec(wikitext)) !== null) {
// Double-check it's not a named ref that our pattern somehow caught
if (!/<ref\s+name\s*=/.test(match[0])) {
refs.anonymous.push({
fullMatch: match[0],
content: match[1],
index: match.index
});
}
}
return refs;
}
/**
* Extract URL from ref content
*/
function extractUrl(content) {
// Try {{Cite chapter|url=...}} format first
var citeMatch = /\{\{Cite\s*chapter\s*\|\s*url\s*=\s*([^\s\}\|]+)/i.exec(content);
if (citeMatch) {
return citeMatch[1];
}
// Try {{Cite web|url=...}} format
var webMatch = /\{\{Cite\s*web\s*\|\s*url\s*=\s*([^\s\}\|]+)/i.exec(content);
if (webMatch) {
return webMatch[1];
}
// Try raw URL
var urlMatch = /(https?:\/\/[^\s\}<]+)/.exec(content);
if (urlMatch) {
return urlMatch[1];
}
return null;
}
/**
* Format a URL as proper citation
*/
function formatCitation(url) {
if (!url) return null;
// Check if it's a chapter URL
if (config.chapterUrlPattern.test(url)) {
return '{{Cite chapter|url=' + url + '}}';
}
// For other site URLs, still use Cite chapter (adjust if you have other templates)
if (config.siteUrlPattern.test(url)) {
return '{{Cite chapter|url=' + url + '}}';
}
// For external URLs, could use Cite web
return url;
}
/**
* Validate a chapter URL has proper format
*/
function validateChapterUrl(url) {
if (!url) return { valid: false, error: 'No URL found' };
var looseMatch = config.chapterUrlLoose.exec(url);
if (!looseMatch) {
return { valid: true, error: null }; // Not a chapter URL, skip validation
}
// It's a chapter URL - check if it has /pXX
if (!looseMatch[2]) {
return {
valid: false,
error: 'Chapter URL missing page number: ' + url + ' (needs /pXX)'
};
}
return { valid: true, error: null };
}
/**
* Find order issues - refs used before they're defined
*/
function findOrderIssues(refs) {
var issues = [];
var definitionsByName = {};
// Index definitions by name
refs.definitions.forEach(function(def) {
if (!definitionsByName[def.name]) {
definitionsByName[def.name] = def;
}
});
// Check each reuse
refs.reuses.forEach(function(reuse) {
var def = definitionsByName[reuse.name];
if (def && reuse.index < def.index) {
issues.push({
name: reuse.name,
reuseIndex: reuse.index,
definitionIndex: def.index,
definition: def,
reuse: reuse
});
}
});
return issues;
}
/**
* Find undefined refs - names used but never defined
*/
function findUndefinedRefs(refs) {
var definedNames = new Set(refs.definitions.map(function(d) { return d.name; }));
var undefinedRefs = [];
refs.reuses.forEach(function(reuse) {
if (!definedNames.has(reuse.name)) {
// Check if we already logged this name
if (!undefinedRefs.some(function(u) { return u.name === reuse.name; })) {
undefinedRefs.push({
name: reuse.name,
usages: refs.reuses.filter(function(r) { return r.name === reuse.name; })
});
}
}
});
return undefinedRefs;
}
/**
* Find anonymous refs that could match undefined names
*/
function findPotentialMatches(refs, undefinedRefs) {
var matches = [];
undefinedRefs.forEach(function(undef) {
refs.anonymous.forEach(function(anon) {
var url = extractUrl(anon.content);
if (url) {
matches.push({
undefinedName: undef.name,
anonymousRef: anon,
url: url
});
}
});
});
return matches;
}
/**
* Fix order issues in wikitext
*/
function fixOrderIssues(wikitext, issues) {
if (issues.length === 0) return wikitext;
// Sort issues by definition index descending (fix from end to preserve indices)
issues.sort(function(a, b) { return b.definitionIndex - a.definitionIndex; });
issues.forEach(function(issue) {
// Strategy:
// 1. Replace the definition with a reuse
// 2. Replace the first reuse with the definition
var def = issue.definition;
var reuse = issue.reuse;
// Build the replacement strings
var reuseStr = '<ref name="' + def.name + '" />';
var defStr = '<ref name="' + def.name + '">' + def.content + '</ref>';
// Replace definition with reuse (do this first since it's later in the text)
wikitext = wikitext.substring(0, def.index) +
reuseStr +
wikitext.substring(def.index + def.fullMatch.length);
// Now replace the reuse with definition
// Need to recalculate position since we changed the text
var offset = reuseStr.length - def.fullMatch.length;
var newReuseIndex = reuse.index; // reuse is before def, so no offset needed
wikitext = wikitext.substring(0, newReuseIndex) +
defStr +
wikitext.substring(newReuseIndex + reuse.fullMatch.length);
});
return wikitext;
}
/**
* Standardize citation format
*/
function standardizeCitations(wikitext) {
// Find all refs with raw URLs (not in Cite templates)
var refPattern = /<ref(\s+name\s*=\s*["'][^"']+["'])?\s*>([\s\S]*?)<\/ref>/gi;
wikitext = wikitext.replace(refPattern, function(match, nameAttr, content) {
nameAttr = nameAttr || '';
// Check if content is just a raw URL
var trimmedContent = content.trim();
// Skip if already in Cite template
if (/\{\{Cite/i.test(trimmedContent)) {
return match;
}
// Check if it's a raw URL to our site
if (config.siteUrlPattern.test(trimmedContent)) {
var url = trimmedContent;
var formatted = formatCitation(url);
return '<ref' + nameAttr + '>' + formatted + '</ref>';
}
return match;
});
return wikitext;
}
/**
* Main fix function
*/
function fixCitations(wikitext) {
var issues = [];
var warnings = [];
var fixes = [];
// Parse all refs
var refs = parseRefs(wikitext);
// 1. Find and fix order issues
var orderIssues = findOrderIssues(refs);
if (orderIssues.length > 0) {
wikitext = fixOrderIssues(wikitext, orderIssues);
orderIssues.forEach(function(issue) {
fixes.push('Fixed order: "' + issue.name + '" - moved definition before first usage');
});
// Re-parse after fixes
refs = parseRefs(wikitext);
}
// 2. Standardize citation format
var before = wikitext;
wikitext = standardizeCitations(wikitext);
if (wikitext !== before) {
fixes.push('Standardized raw URLs to {{Cite chapter|url=...}} format');
refs = parseRefs(wikitext);
}
// 3. Find undefined refs
var undefinedRefs = findUndefinedRefs(refs);
if (undefinedRefs.length > 0) {
undefinedRefs.forEach(function(undef) {
warnings.push('Undefined reference: "' + undef.name + '" is used ' +
undef.usages.length + ' time(s) but never defined');
});
}
// 4. Validate chapter URLs
refs.definitions.forEach(function(def) {
var url = extractUrl(def.content);
var validation = validateChapterUrl(url);
if (!validation.valid) {
warnings.push('Invalid URL in "' + def.name + '": ' + validation.error);
}
});
refs.anonymous.forEach(function(anon, i) {
var url = extractUrl(anon.content);
var validation = validateChapterUrl(url);
if (!validation.valid) {
warnings.push('Invalid URL in anonymous ref #' + (i+1) + ': ' + validation.error);
}
});
// 5. If there are undefined refs, try to help match them
if (undefinedRefs.length > 0 && refs.anonymous.length > 0) {
warnings.push('');
warnings.push('=== Potential matches for undefined refs ===');
warnings.push('Anonymous refs that could be assigned names:');
refs.anonymous.forEach(function(anon, i) {
var url = extractUrl(anon.content);
warnings.push(' Anonymous #' + (i+1) + ': ' + (url || anon.content.substring(0, 50)));
});
warnings.push('');
warnings.push('To fix: Add name="X" to the anonymous ref that should define each name.');
}
return {
wikitext: wikitext,
fixes: fixes,
warnings: warnings
};
}
/**
* Interactive undefined ref fixer
*/
function interactiveFixUndefined(wikitext) {
var refs = parseRefs(wikitext);
var undefinedRefs = findUndefinedRefs(refs);
if (undefinedRefs.length === 0) {
return { wikitext: wikitext, message: 'No undefined references found.' };
}
// For each undefined ref, prompt user to select an anonymous ref
undefinedRefs.forEach(function(undef) {
var options = ['[Skip - leave undefined]', '[Create placeholder]'];
refs.anonymous.forEach(function(anon, i) {
var url = extractUrl(anon.content);
options.push('Anonymous #' + (i+1) + ': ' + (url || anon.content.substring(0, 60)));
});
var message = 'Reference "' + undef.name + '" is used but never defined.\n\n' +
'Select which anonymous ref should define it:\n\n' +
options.map(function(o, i) { return i + ': ' + o; }).join('\n');
var choice = prompt(message, '0');
if (choice === null) return; // Cancelled
var choiceNum = parseInt(choice, 10);
if (choiceNum === 1) {
// Create placeholder - find first usage and replace with definition
var firstUsage = undef.usages[0];
var placeholder = '<ref name="' + undef.name + '">{{Cite chapter|url=UNKNOWN_URL_PLEASE_FIX}}</ref>';
wikitext = wikitext.substring(0, firstUsage.index) +
placeholder +
wikitext.substring(firstUsage.index + firstUsage.fullMatch.length);
} else if (choiceNum >= 2) {
// Assign name to selected anonymous ref
var anonIndex = choiceNum - 2;
var anon = refs.anonymous[anonIndex];
if (anon) {
var namedRef = '<ref name="' + undef.name + '">' + anon.content + '</ref>';
wikitext = wikitext.substring(0, anon.index) +
namedRef +
wikitext.substring(anon.index + anon.fullMatch.length);
// Re-parse since we modified the text
refs = parseRefs(wikitext);
}
}
});
return { wikitext: wikitext, message: 'Interactive fix complete.' };
}
/**
* Show result message using mw.notify (nicer than alert)
*/
function showResultMessage(result) {
var message = '';
if (result.fixes.length > 0) {
message += 'FIXES APPLIED:\n' + result.fixes.join('\n') + '\n\n';
}
if (result.warnings.length > 0) {
message += 'WARNINGS:\n' + result.warnings.join('\n');
}
if (result.fixes.length === 0 && result.warnings.length === 0) {
message = 'No issues found! Citations look good.';
}
// Use mw.notify for a nicer notification, fall back to alert
if (typeof mw !== 'undefined' && mw.notify) {
mw.notify(message.replace(/\n/g, '<br>'), {
title: 'FormattingFixer',
autoHide: false,
tag: 'formattingfixer'
});
} else {
alert(message);
}
}
// ========================================
// VisualEditor Integration
// ========================================
function veIsAvailable() {
return typeof ve !== 'undefined' && ve.init && ve.init.target;
}
function veGetSurface() {
if ( !veIsAvailable() ) {
return null;
}
return ve.init.target.getSurface && ve.init.target.getSurface();
}
function veGetMode() {
var surface = veGetSurface();
return surface && surface.getMode ? surface.getMode() : null;
}
function veGetFullWikitextFromSourceSurface( surface ) {
var model = surface.getModel();
var doc = model.getDocument();
var range = new ve.Range( 0, doc.data.getLength() );
return doc.data.getSourceText( range );
}
function veReplaceAllWikitextInSourceSurface( surface, newWikitext ) {
var model = surface.getModel();
model.getLinearFragment( new ve.Range( 0 ), true )
.expandLinearSelection( 'root' )
.insertContent( newWikitext );
}
function veCreateDmDocumentFromWikitext( wikitext, targetDoc ) {
return ve.init.target.parseWikitextFragment( wikitext, false, targetDoc ).then( function ( response ) {
if ( ve.getProp( response, 'visualeditor', 'result' ) !== 'success' ) {
return $.Deferred().reject( response ).promise();
}
var html = response.visualeditor.content;
var htmlDoc = ve.createDocumentFromHtml( html );
// Mirror VE's own clipboard importer flow for Parsoid HTML
if ( mw.libs && mw.libs.ve && mw.libs.ve.stripRestbaseIds ) {
mw.libs.ve.stripRestbaseIds( htmlDoc );
}
if ( mw.libs && mw.libs.ve && mw.libs.ve.stripParsoidFallbackIds ) {
mw.libs.ve.stripParsoidFallbackIds( htmlDoc.body );
}
// Pass an empty object for importRules to enable clipboard mode
var newDoc = targetDoc.newFromHtml( htmlDoc, {} );
var data = newDoc.data.data;
var surface = new ve.dm.Surface( newDoc );
// Filter out auto-generated items (e.g. reference lists)
for ( var i = data.length - 1; i >= 0; i-- ) {
if ( ve.getProp( data[ i ], 'attributes', 'mw', 'autoGenerated' ) ) {
surface.change(
ve.dm.TransactionBuilder.static.newFromRemoval(
newDoc,
surface.getDocument().getDocumentNode().getNodeFromOffset( i + 1 ).getOuterRange()
)
);
}
}
// Avoid about attribute conflicts
newDoc.data.cloneElements( true );
return newDoc;
} );
}
function veReplaceAllContentWithDocument( uiSurface, newDoc ) {
var surfaceModel = uiSurface.getModel();
surfaceModel.getLinearFragment( new ve.Range( 0 ), true )
.expandLinearSelection( 'root' )
.insertDocument( newDoc );
}
function veEnsureSourceMode() {
var target = ve.init.target;
var surface = veGetSurface();
if ( surface && surface.getMode && surface.getMode() === 'source' ) {
return $.Deferred().resolve().promise();
}
// Switch to the VisualEditor wikitext mode. This is the only reliable
// way to apply whole-document wikitext transforms.
try {
return target.switchToWikitextEditor( true );
} catch ( e ) {
return $.Deferred().reject( e ).promise();
}
}
function runFormattingFixerInVE( mode ) {
if ( !veIsAvailable() ) {
mw.notify( 'FormattingFixer: VisualEditor is not available yet.', { type: 'error' } );
return;
}
var target = ve.init.target;
var surface = veGetSurface();
if ( !surface ) {
mw.notify( 'FormattingFixer: Could not access the editor surface.', { type: 'error' } );
return;
}
var currentMode = veGetMode();
var doc = surface.getModel().getDocument();
// In source mode, we can read/write directly.
if ( currentMode === 'source' ) {
var sourceWikitext = veGetFullWikitextFromSourceSurface( surface );
processWikitext( sourceWikitext, mode, function ( newText ) {
if ( newText !== sourceWikitext ) {
veReplaceAllWikitextInSourceSurface( surface, newText );
}
} );
return;
}
// In visual mode, serialize to wikitext, process, then switch to source
// mode and apply the updated wikitext by parsing it back into VE.
target.getWikitextFragment( doc ).then( function ( wikitext ) {
processWikitext( wikitext, mode, function ( newText ) {
if ( newText === wikitext ) {
return;
}
veCreateDmDocumentFromWikitext( newText, doc ).then( function ( newDoc ) {
veReplaceAllContentWithDocument( surface, newDoc );
}, function () {
mw.notify( 'FormattingFixer: Could not apply changes in VisualEditor. Try using "Edit source".', { type: 'error' } );
} );
} );
} );
}
/**
* Add toolbar buttons to VisualEditor
*/
function addVisualEditorButtons() {
function registerTools() {
// Define and register tools. Use the documented pattern:
// register tools via ve.ui.toolFactory, and add a toolbar group
// via target.static.toolbarGroups (early) or toolbar.addItems (late).
if ( !veIsAvailable() ) {
return;
}
var toolFactory = ve.ui.toolFactory;
if ( !toolFactory.lookup( 'formattingFixerFix' ) ) {
function FormattingFixerFixTool() {
FormattingFixerFixTool.super.apply( this, arguments );
}
OO.inheritClass( FormattingFixerFixTool, OO.ui.Tool );
FormattingFixerFixTool.static.name = 'formattingFixerFix';
FormattingFixerFixTool.static.group = 'formattingfixer';
FormattingFixerFixTool.static.title = 'Fix Citations';
FormattingFixerFixTool.static.icon = 'check';
FormattingFixerFixTool.static.autoAddToCatchall = false;
FormattingFixerFixTool.static.autoAddToGroup = true;
FormattingFixerFixTool.prototype.onSelect = function () {
runFormattingFixerInVE( 'fix' );
this.setActive( false );
};
FormattingFixerFixTool.prototype.onUpdateState = function () {
this.setDisabled( false );
};
toolFactory.register( FormattingFixerFixTool );
}
if ( !toolFactory.lookup( 'formattingFixerUndefined' ) ) {
function FormattingFixerUndefinedTool() {
FormattingFixerUndefinedTool.super.apply( this, arguments );
}
OO.inheritClass( FormattingFixerUndefinedTool, OO.ui.Tool );
FormattingFixerUndefinedTool.static.name = 'formattingFixerUndefined';
FormattingFixerUndefinedTool.static.group = 'formattingfixer';
FormattingFixerUndefinedTool.static.title = 'Fix Undefined Refs';
FormattingFixerUndefinedTool.static.icon = 'link';
FormattingFixerUndefinedTool.static.autoAddToCatchall = false;
FormattingFixerUndefinedTool.static.autoAddToGroup = true;
FormattingFixerUndefinedTool.prototype.onSelect = function () {
runFormattingFixerInVE( 'interactive' );
this.setActive( false );
};
FormattingFixerUndefinedTool.prototype.onUpdateState = function () {
this.setDisabled( false );
};
toolFactory.register( FormattingFixerUndefinedTool );
}
}
// Ensure our tool group is available in the toolbar (early-load path).
function addGroupToTarget( targetClass ) {
if ( !targetClass.static.toolbarGroups ) {
return;
}
var exists = targetClass.static.toolbarGroups.some( function ( g ) {
return g && g.name === 'formattingfixer';
} );
if ( exists ) {
return;
}
targetClass.static.toolbarGroups.push( {
name: 'formattingfixer',
label: 'FormattingFixer',
type: 'list',
indicator: 'down',
include: [ { group: 'formattingfixer' } ]
} );
}
// Preferred: integrate during VE module loading.
mw.hook( 've.loadModules' ).add( function ( addPlugin ) {
addPlugin( function () {
registerTools();
mw.loader.using( [ 'ext.visualEditor.mediawiki' ] ).then( function () {
for ( var n in ve.init.mw.targetFactory.registry ) {
addGroupToTarget( ve.init.mw.targetFactory.lookup( n ) );
}
ve.init.mw.targetFactory.on( 'register', function ( name, targetClass ) {
addGroupToTarget( targetClass );
} );
} );
} );
} );
// Fallback: if VE is already active, add a toolgroup to the existing toolbar.
mw.hook( 've.activationComplete' ).add( function () {
if ( !veIsAvailable() ) {
return;
}
registerTools();
var toolbar = ve.init.target.getToolbar && ve.init.target.getToolbar();
if ( !toolbar ) {
toolbar = ve.init.target.toolbar;
}
if ( !toolbar || toolbar.$element.find( '.formattingfixer-toolgroup' ).length ) {
return;
}
var myToolGroup = new OO.ui.ListToolGroup( toolbar, {
label: 'FormattingFixer',
include: [ 'formattingFixerFix', 'formattingFixerUndefined' ]
} );
myToolGroup.$element.addClass( 'formattingfixer-toolgroup' );
toolbar.addItems( [ myToolGroup ] );
} );
}
/**
* Process wikitext and call callback with result
*/
function processWikitext(wikitext, mode, callback) {
var result;
if (mode === 'fix') {
result = fixCitations(wikitext);
showResultMessage(result);
callback(result.wikitext);
} else if (mode === 'interactive') {
result = interactiveFixUndefined(wikitext);
if (typeof mw !== 'undefined' && mw.notify) {
mw.notify(result.message, { title: 'FormattingFixer', tag: 'formattingfixer' });
} else {
alert(result.message);
}
callback(result.wikitext);
}
}
// ========================================
// WikiEditor Integration (Legacy)
// ========================================
/**
* Add toolbar button for WikiEditor
*/
function addWikiEditorButtons() {
if (typeof $ === 'undefined' || !$.fn.wikiEditor) {
console.log('FormattingFixer: WikiEditor not available');
return false;
}
var $textarea = $('#wpTextbox1');
if ($textarea.length === 0) {
console.log('FormattingFixer: No textarea found');
return false;
}
try {
$textarea.wikiEditor('addToToolbar', {
section: 'advanced',
group: 'format',
tools: {
'formattingfixer-fix': {
label: 'Fix Citations',
type: 'button',
icon: '//upload.wikimedia.org/wikipedia/commons/thumb/4/4a/Commons-emblem-success.svg/22px-Commons-emblem-success.svg.png',
action: {
type: 'callback',
execute: function() {
var textarea = document.getElementById('wpTextbox1');
var result = fixCitations(textarea.value);
textarea.value = result.wikitext;
showResultMessage(result);
}
}
},
'formattingfixer-undefined': {
label: 'Fix Undefined Refs',
type: 'button',
icon: '//upload.wikimedia.org/wikipedia/commons/thumb/e/ea/Disambig_colour.svg/22px-Disambig_colour.svg.png',
action: {
type: 'callback',
execute: function() {
var textarea = document.getElementById('wpTextbox1');
var result = interactiveFixUndefined(textarea.value);
textarea.value = result.wikitext;
if (typeof mw !== 'undefined' && mw.notify) {
mw.notify(result.message, { title: 'FormattingFixer' });
} else {
alert(result.message);
}
}
}
}
}
});
console.log('FormattingFixer: WikiEditor buttons added');
return true;
} catch (e) {
console.log('FormattingFixer: Error adding WikiEditor buttons:', e);
return false;
}
}
// ========================================
// Fallback: Simple Buttons
// ========================================
/**
* Add simple HTML buttons as fallback
*/
function addSimpleButtons() {
var textbox = document.getElementById('wpTextbox1');
if (!textbox) return false;
// Don't attach to VisualEditor's hidden dummy textbox
if (textbox.classList && textbox.classList.contains('ve-dummyTextbox')) {
return false;
}
// Check if buttons already exist
if (document.getElementById('formattingfixer-buttons')) return true;
var container = document.createElement('div');
container.id = 'formattingfixer-buttons';
container.style.cssText = 'margin: 5px 0; padding: 8px; background: #f8f9fa; border: 1px solid #a2a9b1; border-radius: 2px; display: flex; gap: 10px; align-items: center;';
var label = document.createElement('span');
label.textContent = 'FormattingFixer:';
label.style.fontWeight = 'bold';
container.appendChild(label);
var fixBtn = document.createElement('button');
fixBtn.textContent = '🔧 Fix Citations';
fixBtn.style.cssText = 'padding: 5px 10px; cursor: pointer; border: 1px solid #a2a9b1; border-radius: 2px; background: #fff;';
fixBtn.type = 'button';
fixBtn.onclick = function() {
var result = fixCitations(textbox.value);
textbox.value = result.wikitext;
showResultMessage(result);
};
container.appendChild(fixBtn);
var interactiveBtn = document.createElement('button');
interactiveBtn.textContent = '🔗 Fix Undefined Refs';
interactiveBtn.style.cssText = 'padding: 5px 10px; cursor: pointer; border: 1px solid #a2a9b1; border-radius: 2px; background: #fff;';
interactiveBtn.type = 'button';
interactiveBtn.onclick = function() {
var result = interactiveFixUndefined(textbox.value);
textbox.value = result.wikitext;
if (typeof mw !== 'undefined' && mw.notify) {
mw.notify(result.message, { title: 'FormattingFixer' });
} else {
alert(result.message);
}
};
container.appendChild(interactiveBtn);
textbox.parentNode.insertBefore(container, textbox);
console.log('FormattingFixer: Simple buttons added');
return true;
}
// ========================================
// Initialization
// ========================================
function init() {
var action = mw.config.get('wgAction');
var veLoaded = mw.config.get('wgVisualEditor');
console.log('FormattingFixer: Initializing, action=' + action);
// For VisualEditor
if (typeof ve !== 'undefined' || (veLoaded && veLoaded.pageLanguageDir)) {
console.log('FormattingFixer: Setting up VisualEditor hooks');
addVisualEditorButtons();
}
// Hook for when VE loads later
mw.hook('ve.activationComplete').add(function() {
console.log('FormattingFixer: VE activation complete');
addVisualEditorButtons();
});
// For source editing (action=edit without VE)
if (action === 'edit' || action === 'submit') {
// Try WikiEditor first
mw.hook('wikiEditor.toolbarReady').add(function($textarea) {
console.log('FormattingFixer: WikiEditor toolbar ready');
addWikiEditorButtons();
});
// If this is the classic source editor, try loading WikiEditor explicitly.
// (If the user doesn't have it enabled, we'll fall back to simple buttons.)
setTimeout(function() {
var textbox = document.getElementById('wpTextbox1');
if (!textbox) {
return;
}
if (textbox.classList && textbox.classList.contains('ve-dummyTextbox')) {
return;
}
mw.loader.using(['ext.wikiEditor']).then(function() {
addWikiEditorButtons();
}, function() {
addSimpleButtons();
});
}, 0);
// If WikiEditor isn't present, ensure the user still gets controls
// in the classic source editor.
setTimeout(function() {
var textbox = document.getElementById('wpTextbox1');
if (textbox && !document.getElementById('formattingfixer-buttons')) {
if (textbox.classList && textbox.classList.contains('ve-dummyTextbox')) {
return;
}
if (typeof $ !== 'undefined' && $.fn && $.fn.wikiEditor) {
// WikiEditor exists but may not be initialized yet.
if (!addWikiEditorButtons()) {
addSimpleButtons();
}
} else {
addSimpleButtons();
}
}
}, 250);
// Fallback after delay
setTimeout(function() {
var textbox = document.getElementById('wpTextbox1');
if (textbox && !document.getElementById('formattingfixer-buttons')) {
if (textbox.classList && textbox.classList.contains('ve-dummyTextbox')) {
return;
}
// Check if WikiEditor toolbar exists
if ($('.wikiEditor-ui-toolbar').length > 0) {
if (!addWikiEditorButtons()) {
addSimpleButtons();
}
} else {
addSimpleButtons();
}
}
}, 1500);
}
}
// Run when ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function() {
mw.loader.using(['mediawiki.util']).then(init);
});
} else {
mw.loader.using(['mediawiki.util']).then(init);
}
// Expose for testing
window.FormattingFixer = {
fixCitations: fixCitations,
parseRefs: parseRefs,
interactiveFixUndefined: interactiveFixUndefined
};
})();