MediaWiki:Gadget-FormattingFixer.js: Difference between revisions
Fix VE switch flakiness: poll for wpTextbox1 and apply regardless of promise rejection Tag: Reverted |
FormattingFixer: nicer notify + preview refresh; chapter-only citation; add Links actions (stub) Tag: Reverted |
||
| Line 30: | Line 30: | ||
// Guard to prevent duplicate WikiEditor buttons | // Guard to prevent duplicate WikiEditor buttons | ||
var wikiEditorButtonsAdded = false; | var wikiEditorButtonsAdded = false; | ||
function getNotifyType( result ) { | |||
return result && result.warnings && result.warnings.length ? 'warn' : 'success'; | |||
} | |||
/** | /** | ||
| Line 119: | Line 123: | ||
/** | /** | ||
* Format a URL as proper citation | * Format a URL as proper citation | ||
*/ | */ | ||
function formatCitation(url) { | function formatCitation(url) { | ||
if (!url) return null; | if (!url) return null; | ||
// | // Check if it's a chapter URL | ||
if (config.chapterUrlPattern.test(url)) { | if (config.chapterUrlPattern.test(url)) { | ||
return '{{Cite chapter|url=' + url + '}}'; | return '{{Cite chapter|url=' + url + '}}'; | ||
} | } | ||
return url; | return url; | ||
} | } | ||
| Line 236: | Line 239: | ||
var usedNames = new Set(refs.definitions.map(function(d) { return d.name; })); | var usedNames = new Set(refs.definitions.map(function(d) { return d.name; })); | ||
var fixes = []; | var fixes = []; | ||
var changed = 0; | |||
// Sort by index descending to preserve positions when replacing | // Sort by index descending to preserve positions when replacing | ||
| Line 261: | Line 265: | ||
wikitext = wikitext.substring(0, anon.index) + namedRef + wikitext.substring(anon.index + anon.fullMatch.length); | wikitext = wikitext.substring(0, anon.index) + namedRef + wikitext.substring(anon.index + anon.fullMatch.length); | ||
fixes.push('Named anonymous ref as "' + name + '"'); | fixes.push('Named anonymous ref as "' + name + '"'); | ||
changed++; | |||
}); | }); | ||
return { wikitext: wikitext, fixes: fixes }; | return { wikitext: wikitext, fixes: fixes, changed: changed }; | ||
} | } | ||
| Line 306: | Line 311: | ||
/** | /** | ||
* Standardize citation format | * Standardize citation format | ||
*/ | */ | ||
function standardizeCitations(wikitext) { | function standardizeCitations(wikitext) { | ||
var changed = 0; | |||
// Find all refs with raw URLs (not in Cite templates) | // Find all refs with raw URLs (not in Cite templates) | ||
var refPattern = /<ref(\s+name\s*=\s*["'][^"']+["'])?\s*>([\s\S]*?)<\/ref>/gi; | var refPattern = /<ref(\s+name\s*=\s*["'][^"']+["'])?\s*>([\s\S]*?)<\/ref>/gi; | ||
wikitext = wikitext.replace(refPattern, function(match, nameAttr, content) { | wikitext = wikitext.replace(refPattern, function(match, nameAttr, content) { | ||
| Line 324: | Line 329: | ||
} | } | ||
// | // Only standardize proper chapter/page URLs | ||
if (config.chapterUrlPattern.test(trimmedContent)) { | if (config.chapterUrlPattern.test(trimmedContent)) { | ||
var url = trimmedContent; | var url = trimmedContent; | ||
var formatted = formatCitation(url); | var formatted = formatCitation(url); | ||
changed++; | |||
return '<ref' + nameAttr + '>' + formatted + '</ref>'; | return '<ref' + nameAttr + '>' + formatted + '</ref>'; | ||
} | } | ||
| Line 335: | Line 340: | ||
}); | }); | ||
return { wikitext: wikitext, | return { wikitext: wikitext, changed: changed }; | ||
} | } | ||
| Line 345: | Line 350: | ||
var warnings = []; | var warnings = []; | ||
var fixes = []; | var fixes = []; | ||
var changeCount = 0; | |||
// Parse all refs | // Parse all refs | ||
| Line 356: | Line 362: | ||
fixes.push('Fixed order: "' + issue.name + '" - moved definition before first usage'); | fixes.push('Fixed order: "' + issue.name + '" - moved definition before first usage'); | ||
}); | }); | ||
changeCount += orderIssues.length; | |||
// Re-parse after fixes | // Re-parse after fixes | ||
refs = parseRefs(wikitext); | refs = parseRefs(wikitext); | ||
} | } | ||
// 2. Standardize citation format | // 2. Standardize citation format | ||
var | var citationResult = standardizeCitations(wikitext); | ||
if ( | if (citationResult.wikitext !== wikitext) { | ||
wikitext = | wikitext = citationResult.wikitext; | ||
fixes.push('Standardized | fixes.push('Standardized raw chapter URLs to {{Cite chapter|url=...}} format'); | ||
changeCount += citationResult.changed; | |||
refs = parseRefs(wikitext); | refs = parseRefs(wikitext); | ||
} | } | ||
| Line 398: | Line 406: | ||
wikitext = namingResult.wikitext; | wikitext = namingResult.wikitext; | ||
fixes = fixes.concat(namingResult.fixes); | fixes = fixes.concat(namingResult.fixes); | ||
changeCount += namingResult.changed; | |||
refs = parseRefs(wikitext); | refs = parseRefs(wikitext); | ||
} | } | ||
| Line 417: | Line 426: | ||
warnings: warnings | warnings: warnings | ||
}; | }; | ||
} | |||
function autoAddLinks( wikitext ) { | |||
return { | |||
wikitext: wikitext, | |||
fixes: [], | |||
warnings: [] | |||
}; | |||
} | |||
function runActionOnWikitext( wikitext, action ) { | |||
var result; | |||
var combinedFixes = []; | |||
var combinedWarnings = []; | |||
var before = wikitext; | |||
if ( action === 'links' ) { | |||
result = autoAddLinks( wikitext ); | |||
} else if ( action === 'fixlinks' ) { | |||
result = fixCitations( wikitext ); | |||
combinedFixes = combinedFixes.concat( result.fixes || [] ); | |||
combinedWarnings = combinedWarnings.concat( result.warnings || [] ); | |||
wikitext = result.wikitext; | |||
result = autoAddLinks( wikitext ); | |||
combinedFixes = combinedFixes.concat( result.fixes || [] ); | |||
combinedWarnings = combinedWarnings.concat( result.warnings || [] ); | |||
result = { wikitext: result.wikitext, fixes: combinedFixes, warnings: combinedWarnings }; | |||
} else { | |||
result = fixCitations( wikitext ); | |||
} | |||
result._changed = ( result.wikitext !== before ); | |||
return result; | |||
} | } | ||
/** | /** | ||
* Show result message using mw.notify | * Show result message using mw.notify (nicer than alert) | ||
*/ | */ | ||
function showResultMessage(result) { | function showResultMessage(result) { | ||
var | var fixCount = (result && result.fixes) ? result.fixes.length : 0; | ||
var warnCount = (result && result.warnings) ? result.warnings.length : 0; | |||
var summary; | |||
if ( fixCount === 0 && warnCount === 0 ) { | |||
summary = 'No changes needed.'; | |||
} else { | |||
summary = 'Applied ' + fixCount + ' change(s)'; | |||
if ( warnCount ) { | |||
summary += '; ' + warnCount + ' warning(s)'; | |||
} | |||
summary += '.'; | |||
} | |||
if ( result && result.fixes && result.fixes.length ) { | |||
console.log( 'FormattingFixer fixes:', result.fixes ); | |||
} | |||
if ( result && result.warnings && result.warnings.length ) { | |||
console.warn( 'FormattingFixer warnings:', result.warnings ); | |||
} | |||
if ( | // Use mw.notify for a nicer notification, fall back to alert | ||
mw.notify( | if (typeof mw !== 'undefined' && mw.notify) { | ||
mw.notify( summary, { | |||
title: 'FormattingFixer', | title: 'FormattingFixer', | ||
autoHide: true, | |||
type: getNotifyType( result ), | |||
tag: 'formattingfixer' | tag: 'formattingfixer' | ||
} | } ); | ||
} else { | |||
alert(summary); | |||
} | |||
} | } | ||
} | } | ||
| Line 597: | Line 591: | ||
// way to apply whole-document wikitext transforms. | // way to apply whole-document wikitext transforms. | ||
try { | try { | ||
return target.switchToWikitextEditor( true ); | |||
} catch ( e ) { | } catch ( e ) { | ||
return $.Deferred().reject( e ).promise(); | return $.Deferred().reject( e ).promise(); | ||
| Line 625: | Line 597: | ||
} | } | ||
function runFormattingFixerInVE( action ) { | |||
if ( !veIsAvailable() ) { | if ( !veIsAvailable() ) { | ||
mw.notify( 'FormattingFixer: VisualEditor is not available yet.', { type: 'error' } ); | mw.notify( 'FormattingFixer: VisualEditor is not available yet.', { type: 'error' } ); | ||
| Line 645: | Line 614: | ||
// In source mode, we can read/write directly. | // In source mode, we can read/write directly. | ||
if ( currentMode === 'source' ) { | if ( currentMode === 'source' ) { | ||
var | var sourceWikitext = veGetFullWikitextFromSourceSurface( surface ); | ||
var result = runActionOnWikitext( sourceWikitext, action ); | |||
if ( result.wikitext !== sourceWikitext ) { | |||
veReplaceAllWikitextInSourceSurface( surface, result.wikitext ); | |||
veReplaceAllWikitextInSourceSurface( surface, | |||
} | } | ||
showResultMessage( | showResultMessage( result ); | ||
return; | return; | ||
} | } | ||
// In visual mode, switch to source mode first to avoid Parsoid round-trip | // In visual mode, switch to source mode first to avoid Parsoid round-trip | ||
// which would collapse multiline templates and remove underscores from filenames | |||
mw.notify( 'FormattingFixer: Switching to source mode to preserve formatting...', { tag: 'formattingfixer' } ); | mw.notify( 'FormattingFixer: Switching to source mode to preserve formatting...', { tag: 'formattingfixer' } ); | ||
veEnsureSourceMode().then( function () { | |||
var newSurface = veGetSurface(); | |||
if ( !newSurface || veGetMode() !== 'source' ) { | |||
mw.notify( 'FormattingFixer: Could not switch to source mode.', { type: 'error' } ); | |||
var | |||
if ( | |||
return; | return; | ||
} | } | ||
var wikitext = veGetFullWikitextFromSourceSurface( newSurface ); | |||
if ( | var result = runActionOnWikitext( wikitext, action ); | ||
if ( result.wikitext !== wikitext ) { | |||
veReplaceAllWikitextInSourceSurface( newSurface, result.wikitext ); | |||
} | } | ||
showResultMessage( result ); | |||
mw.notify( 'FormattingFixer: | }, function () { | ||
} | mw.notify( 'FormattingFixer: Could not switch to source mode. Please switch manually and try again.', { type: 'error' } ); | ||
} ); | |||
} | } | ||
| Line 730: | Line 658: | ||
var toolFactory = ve.ui.toolFactory; | var toolFactory = ve.ui.toolFactory; | ||
if ( !toolFactory.lookup( 'formattingFixerFix' ) ) { | if ( !toolFactory.lookup( 'formattingFixerFix' ) ) { | ||
function FormattingFixerFixTool() { | function FormattingFixerFixTool() { | ||
| Line 739: | Line 666: | ||
FormattingFixerFixTool.static.group = 'formattingfixer'; | FormattingFixerFixTool.static.group = 'formattingfixer'; | ||
FormattingFixerFixTool.static.title = 'Fix Citations'; | FormattingFixerFixTool.static.title = 'Fix Citations'; | ||
FormattingFixerFixTool.static.icon = ' | FormattingFixerFixTool.static.icon = 'check'; | ||
FormattingFixerFixTool.static.autoAddToCatchall = false; | FormattingFixerFixTool.static.autoAddToCatchall = false; | ||
FormattingFixerFixTool.static.autoAddToGroup = true; | FormattingFixerFixTool.static.autoAddToGroup = true; | ||
FormattingFixerFixTool.prototype.onSelect = function () { | FormattingFixerFixTool.prototype.onSelect = function () { | ||
runFormattingFixerInVE( 'fix' ); | |||
this.setActive( false ); | this.setActive( false ); | ||
}; | }; | ||
| Line 752: | Line 679: | ||
} | } | ||
if ( !toolFactory.lookup( 'formattingFixerLinks' ) ) { | if ( !toolFactory.lookup( 'formattingFixerLinks' ) ) { | ||
function FormattingFixerLinksTool() { | function FormattingFixerLinksTool() { | ||
| Line 765: | Line 691: | ||
FormattingFixerLinksTool.static.autoAddToGroup = true; | FormattingFixerLinksTool.static.autoAddToGroup = true; | ||
FormattingFixerLinksTool.prototype.onSelect = function () { | FormattingFixerLinksTool.prototype.onSelect = function () { | ||
runFormattingFixerInVE( 'links' ); | |||
this.setActive( false ); | this.setActive( false ); | ||
}; | }; | ||
| Line 774: | Line 700: | ||
} | } | ||
if ( !toolFactory.lookup( 'formattingFixerFixLinks' ) ) { | |||
if ( !toolFactory.lookup( ' | function FormattingFixerFixLinksTool() { | ||
function | FormattingFixerFixLinksTool.super.apply( this, arguments ); | ||
} | } | ||
OO.inheritClass( | OO.inheritClass( FormattingFixerFixLinksTool, OO.ui.Tool ); | ||
FormattingFixerFixLinksTool.static.name = 'formattingFixerFixLinks'; | |||
FormattingFixerFixLinksTool.static.group = 'formattingfixer'; | |||
FormattingFixerFixLinksTool.static.title = 'Fix Citations and Auto Add Links'; | |||
FormattingFixerFixLinksTool.static.icon = 'link'; | |||
FormattingFixerFixLinksTool.static.autoAddToCatchall = false; | |||
FormattingFixerFixLinksTool.static.autoAddToGroup = true; | |||
FormattingFixerFixLinksTool.prototype.onSelect = function () { | |||
runFormattingFixerInVE( 'fixlinks' ); | |||
this.setActive( false ); | this.setActive( false ); | ||
}; | }; | ||
FormattingFixerFixLinksTool.prototype.onUpdateState = function () { | |||
this.setDisabled( false ); | this.setDisabled( false ); | ||
}; | }; | ||
toolFactory.register( | toolFactory.register( FormattingFixerFixLinksTool ); | ||
} | } | ||
} | } | ||
| Line 850: | Line 775: | ||
var myToolGroup = new OO.ui.ListToolGroup( toolbar, { | var myToolGroup = new OO.ui.ListToolGroup( toolbar, { | ||
label: 'FormattingFixer', | label: 'FormattingFixer', | ||
include: [ 'formattingFixerFix', 'formattingFixerLinks', ' | include: [ 'formattingFixerFix', 'formattingFixerLinks', 'formattingFixerFixLinks' ] | ||
} ); | } ); | ||
myToolGroup.$element.addClass( 'formattingfixer-toolgroup' ); | myToolGroup.$element.addClass( 'formattingfixer-toolgroup' ); | ||
| Line 896: | Line 821: | ||
label: 'Fix Citations', | label: 'Fix Citations', | ||
type: 'button', | type: 'button', | ||
oouiIcon: ' | oouiIcon: 'check', | ||
action: { | action: { | ||
type: 'callback', | type: 'callback', | ||
execute: function() { | execute: function() { | ||
var textarea = document.getElementById('wpTextbox1'); | var textarea = document.getElementById('wpTextbox1'); | ||
var result = | var result = runActionOnWikitext( textarea.value, 'fix' ); | ||
textarea.value = result.wikitext; | textarea.value = result.wikitext; | ||
if ( typeof $ !== 'undefined' ) { | |||
$( textarea ).trigger( 'input' ).trigger( 'change' ); | |||
} | |||
showResultMessage(result); | showResultMessage(result); | ||
} | } | ||
} | } | ||
| Line 920: | Line 843: | ||
execute: function() { | execute: function() { | ||
var textarea = document.getElementById('wpTextbox1'); | var textarea = document.getElementById('wpTextbox1'); | ||
var result = | var result = runActionOnWikitext( textarea.value, 'links' ); | ||
textarea.value = result.wikitext; | textarea.value = result.wikitext; | ||
if ( typeof $ !== 'undefined' ) { | |||
$( textarea ).trigger( 'input' ).trigger( 'change' ); | |||
} | |||
showResultMessage(result); | showResultMessage(result); | ||
} | } | ||
} | } | ||
}, | }, | ||
'formattingfixer- | 'formattingfixer-fixlinks': { | ||
label: 'Fix | label: 'Fix Citations + Links', | ||
type: 'button', | type: 'button', | ||
oouiIcon: ' | oouiIcon: 'link', | ||
action: { | action: { | ||
type: 'callback', | type: 'callback', | ||
execute: function() { | execute: function() { | ||
var textarea = document.getElementById('wpTextbox1'); | var textarea = document.getElementById('wpTextbox1'); | ||
var result = | var result = runActionOnWikitext( textarea.value, 'fixlinks' ); | ||
textarea.value = result.wikitext; | textarea.value = result.wikitext; | ||
if ( typeof $ !== 'undefined' ) { | |||
$( textarea ).trigger( 'input' ).trigger( 'change' ); | |||
} | |||
showResultMessage(result); | showResultMessage(result); | ||
} | } | ||
} | } | ||
| Line 945: | Line 872: | ||
}); | }); | ||
wikiEditorButtonsAdded = true; | wikiEditorButtonsAdded = true; | ||
console.log('FormattingFixer: WikiEditor | console.log('FormattingFixer: WikiEditor button added'); | ||
return true; | return true; | ||
} catch (e) { | } catch (e) { | ||
| Line 980: | Line 907: | ||
label.style.fontWeight = 'bold'; | label.style.fontWeight = 'bold'; | ||
container.appendChild(label); | container.appendChild(label); | ||
var fixBtn = document.createElement('button'); | var fixBtn = document.createElement('button'); | ||
fixBtn.textContent = 'Fix Citations'; | fixBtn.textContent = 'Fix Citations'; | ||
fixBtn.style.cssText = | fixBtn.style.cssText = 'padding: 5px 10px; cursor: pointer; border: 1px solid #a2a9b1; border-radius: 2px; background: #fff;'; | ||
fixBtn.type = 'button'; | fixBtn.type = 'button'; | ||
fixBtn.onclick = function() { | fixBtn.onclick = function() { | ||
var result = | var result = runActionOnWikitext( textbox.value, 'fix' ); | ||
textbox.value = result.wikitext; | textbox.value = result.wikitext; | ||
if ( typeof $ !== 'undefined' ) { | |||
$( textbox ).trigger( 'input' ).trigger( 'change' ); | |||
} | |||
showResultMessage(result); | showResultMessage(result); | ||
}; | }; | ||
| Line 996: | Line 924: | ||
var linksBtn = document.createElement('button'); | var linksBtn = document.createElement('button'); | ||
linksBtn.textContent = 'Auto Add Links'; | linksBtn.textContent = 'Auto Add Links'; | ||
linksBtn.style.cssText = | linksBtn.style.cssText = 'padding: 5px 10px; cursor: pointer; border: 1px solid #a2a9b1; border-radius: 2px; background: #fff;'; | ||
linksBtn.type = 'button'; | linksBtn.type = 'button'; | ||
linksBtn.onclick = function() { | linksBtn.onclick = function() { | ||
var result = | var result = runActionOnWikitext( textbox.value, 'links' ); | ||
textbox.value = result.wikitext; | textbox.value = result.wikitext; | ||
if ( typeof $ !== 'undefined' ) { | |||
$( textbox ).trigger( 'input' ).trigger( 'change' ); | |||
} | |||
showResultMessage(result); | showResultMessage(result); | ||
}; | }; | ||
container.appendChild(linksBtn); | container.appendChild(linksBtn); | ||
var | var bothBtn = document.createElement('button'); | ||
bothBtn.textContent = 'Fix Citations + Links'; | |||
bothBtn.style.cssText = 'padding: 5px 10px; cursor: pointer; border: 1px solid #a2a9b1; border-radius: 2px; background: #fff;'; | |||
bothBtn.type = 'button'; | |||
bothBtn.onclick = function() { | |||
var result = | var result = runActionOnWikitext( textbox.value, 'fixlinks' ); | ||
textbox.value = result.wikitext; | textbox.value = result.wikitext; | ||
if ( typeof $ !== 'undefined' ) { | |||
$( textbox ).trigger( 'input' ).trigger( 'change' ); | |||
} | |||
showResultMessage(result); | showResultMessage(result); | ||
}; | }; | ||
container.appendChild( | container.appendChild(bothBtn); | ||
textbox.parentNode.insertBefore(container, textbox); | textbox.parentNode.insertBefore(container, textbox); | ||
| Line 1,120: | Line 1,054: | ||
window.FormattingFixer = { | window.FormattingFixer = { | ||
fixCitations: fixCitations, | fixCitations: fixCitations, | ||
parseRefs: parseRefs, | parseRefs: parseRefs, | ||
generateRefName: generateRefName | generateRefName: generateRefName, | ||
autoAddLinks: autoAddLinks | |||
}; | }; | ||
})(); | })(); | ||
Revision as of 10:06, 5 January 2026
/**
* 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\//
};
// Guard to prevent duplicate WikiEditor buttons
var wikiEditorButtonsAdded = false;
function getNotifyType( result ) {
return result && result.warnings && result.warnings.length ? 'warn' : 'success';
}
/**
* 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;
}
/**
* Generate a ref name from a chapter URL (e.g., :c37p15 or :c37_1p15 for decimals)
*/
function generateRefName(url) {
var match = config.chapterUrlPattern.exec(url);
if (!match) return null;
var chapter = match[1];
var page = match[2];
// Replace decimal with underscore for valid ref names (37.1 -> 37_1)
var safeChapter = chapter.replace('.', '_');
return ':c' + safeChapter + 'p' + page;
}
/**
* 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 + '}}';
}
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;
}
/**
* Assign chapter-based names to anonymous refs with chapter URLs
*/
function assignNamesToAnonymousRefs(wikitext, refs) {
var usedNames = new Set(refs.definitions.map(function(d) { return d.name; }));
var fixes = [];
var changed = 0;
// Sort by index descending to preserve positions when replacing
var anonsToName = refs.anonymous.filter(function(anon) {
var url = extractUrl(anon.content);
return url && config.chapterUrlPattern.test(url);
}).sort(function(a, b) { return b.index - a.index; });
anonsToName.forEach(function(anon) {
var url = extractUrl(anon.content);
var baseName = generateRefName(url);
if (!baseName) return;
// Ensure unique name
var name = baseName;
var suffix = 2;
while (usedNames.has(name)) {
name = baseName + '_' + suffix;
suffix++;
}
usedNames.add(name);
// Replace anonymous ref with named ref
var namedRef = '<ref name="' + name + '">' + anon.content + '</ref>';
wikitext = wikitext.substring(0, anon.index) + namedRef + wikitext.substring(anon.index + anon.fullMatch.length);
fixes.push('Named anonymous ref as "' + name + '"');
changed++;
});
return { wikitext: wikitext, fixes: fixes, changed: changed };
}
/**
* 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) {
var changed = 0;
// 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;
}
// Only standardize proper chapter/page URLs
if (config.chapterUrlPattern.test(trimmedContent)) {
var url = trimmedContent;
var formatted = formatCitation(url);
changed++;
return '<ref' + nameAttr + '>' + formatted + '</ref>';
}
return match;
});
return { wikitext: wikitext, changed: changed };
}
/**
* Main fix function
*/
function fixCitations(wikitext) {
var issues = [];
var warnings = [];
var fixes = [];
var changeCount = 0;
// 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');
});
changeCount += orderIssues.length;
// Re-parse after fixes
refs = parseRefs(wikitext);
}
// 2. Standardize citation format
var citationResult = standardizeCitations(wikitext);
if (citationResult.wikitext !== wikitext) {
wikitext = citationResult.wikitext;
fixes.push('Standardized raw chapter URLs to {{Cite chapter|url=...}} format');
changeCount += citationResult.changed;
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. Assign names to anonymous refs with chapter URLs
var namingResult = assignNamesToAnonymousRefs(wikitext, refs);
if (namingResult.fixes.length > 0) {
wikitext = namingResult.wikitext;
fixes = fixes.concat(namingResult.fixes);
changeCount += namingResult.changed;
refs = parseRefs(wikitext);
}
// 6. Re-check undefined refs after naming
undefinedRefs = findUndefinedRefs(refs);
if (undefinedRefs.length > 0) {
warnings.push('');
warnings.push('=== Undefined references requiring manual attention ===');
undefinedRefs.forEach(function(undef) {
warnings.push('Reference "' + undef.name + '" is used ' + undef.usages.length + ' time(s) but never defined.');
warnings.push(' → Find the correct source and add: <ref name="' + undef.name + '">{{Cite chapter|url=...}}</ref>');
});
}
return {
wikitext: wikitext,
fixes: fixes,
warnings: warnings
};
}
function autoAddLinks( wikitext ) {
return {
wikitext: wikitext,
fixes: [],
warnings: []
};
}
function runActionOnWikitext( wikitext, action ) {
var result;
var combinedFixes = [];
var combinedWarnings = [];
var before = wikitext;
if ( action === 'links' ) {
result = autoAddLinks( wikitext );
} else if ( action === 'fixlinks' ) {
result = fixCitations( wikitext );
combinedFixes = combinedFixes.concat( result.fixes || [] );
combinedWarnings = combinedWarnings.concat( result.warnings || [] );
wikitext = result.wikitext;
result = autoAddLinks( wikitext );
combinedFixes = combinedFixes.concat( result.fixes || [] );
combinedWarnings = combinedWarnings.concat( result.warnings || [] );
result = { wikitext: result.wikitext, fixes: combinedFixes, warnings: combinedWarnings };
} else {
result = fixCitations( wikitext );
}
result._changed = ( result.wikitext !== before );
return result;
}
/**
* Show result message using mw.notify (nicer than alert)
*/
function showResultMessage(result) {
var fixCount = (result && result.fixes) ? result.fixes.length : 0;
var warnCount = (result && result.warnings) ? result.warnings.length : 0;
var summary;
if ( fixCount === 0 && warnCount === 0 ) {
summary = 'No changes needed.';
} else {
summary = 'Applied ' + fixCount + ' change(s)';
if ( warnCount ) {
summary += '; ' + warnCount + ' warning(s)';
}
summary += '.';
}
if ( result && result.fixes && result.fixes.length ) {
console.log( 'FormattingFixer fixes:', result.fixes );
}
if ( result && result.warnings && result.warnings.length ) {
console.warn( 'FormattingFixer warnings:', result.warnings );
}
// Use mw.notify for a nicer notification, fall back to alert
if (typeof mw !== 'undefined' && mw.notify) {
mw.notify( summary, {
title: 'FormattingFixer',
autoHide: true,
type: getNotifyType( result ),
tag: 'formattingfixer'
} );
} else {
alert(summary);
}
}
// ========================================
// 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( action ) {
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();
// In source mode, we can read/write directly.
if ( currentMode === 'source' ) {
var sourceWikitext = veGetFullWikitextFromSourceSurface( surface );
var result = runActionOnWikitext( sourceWikitext, action );
if ( result.wikitext !== sourceWikitext ) {
veReplaceAllWikitextInSourceSurface( surface, result.wikitext );
}
showResultMessage( result );
return;
}
// In visual mode, switch to source mode first to avoid Parsoid round-trip
// which would collapse multiline templates and remove underscores from filenames
mw.notify( 'FormattingFixer: Switching to source mode to preserve formatting...', { tag: 'formattingfixer' } );
veEnsureSourceMode().then( function () {
var newSurface = veGetSurface();
if ( !newSurface || veGetMode() !== 'source' ) {
mw.notify( 'FormattingFixer: Could not switch to source mode.', { type: 'error' } );
return;
}
var wikitext = veGetFullWikitextFromSourceSurface( newSurface );
var result = runActionOnWikitext( wikitext, action );
if ( result.wikitext !== wikitext ) {
veReplaceAllWikitextInSourceSurface( newSurface, result.wikitext );
}
showResultMessage( result );
}, function () {
mw.notify( 'FormattingFixer: Could not switch to source mode. Please switch manually and try again.', { 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( 'formattingFixerLinks' ) ) {
function FormattingFixerLinksTool() {
FormattingFixerLinksTool.super.apply( this, arguments );
}
OO.inheritClass( FormattingFixerLinksTool, OO.ui.Tool );
FormattingFixerLinksTool.static.name = 'formattingFixerLinks';
FormattingFixerLinksTool.static.group = 'formattingfixer';
FormattingFixerLinksTool.static.title = 'Auto Add Links';
FormattingFixerLinksTool.static.icon = 'link';
FormattingFixerLinksTool.static.autoAddToCatchall = false;
FormattingFixerLinksTool.static.autoAddToGroup = true;
FormattingFixerLinksTool.prototype.onSelect = function () {
runFormattingFixerInVE( 'links' );
this.setActive( false );
};
FormattingFixerLinksTool.prototype.onUpdateState = function () {
this.setDisabled( false );
};
toolFactory.register( FormattingFixerLinksTool );
}
if ( !toolFactory.lookup( 'formattingFixerFixLinks' ) ) {
function FormattingFixerFixLinksTool() {
FormattingFixerFixLinksTool.super.apply( this, arguments );
}
OO.inheritClass( FormattingFixerFixLinksTool, OO.ui.Tool );
FormattingFixerFixLinksTool.static.name = 'formattingFixerFixLinks';
FormattingFixerFixLinksTool.static.group = 'formattingfixer';
FormattingFixerFixLinksTool.static.title = 'Fix Citations and Auto Add Links';
FormattingFixerFixLinksTool.static.icon = 'link';
FormattingFixerFixLinksTool.static.autoAddToCatchall = false;
FormattingFixerFixLinksTool.static.autoAddToGroup = true;
FormattingFixerFixLinksTool.prototype.onSelect = function () {
runFormattingFixerInVE( 'fixlinks' );
this.setActive( false );
};
FormattingFixerFixLinksTool.prototype.onUpdateState = function () {
this.setDisabled( false );
};
toolFactory.register( FormattingFixerFixLinksTool );
}
}
// 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', 'formattingFixerLinks', 'formattingFixerFixLinks' ]
} );
myToolGroup.$element.addClass( 'formattingfixer-toolgroup' );
toolbar.addItems( [ myToolGroup ] );
} );
}
// ========================================
// WikiEditor Integration (Legacy)
// ========================================
/**
* Add toolbar button for WikiEditor
*/
function addWikiEditorButtons() {
// Guard against duplicate button creation
if (wikiEditorButtonsAdded) {
return true;
}
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;
}
// Check if button already exists in DOM
if ($('.tool[rel="formattingfixer-fix"]').length > 0) {
wikiEditorButtonsAdded = true;
return true;
}
try {
$textarea.wikiEditor('addToToolbar', {
section: 'advanced',
group: 'format',
tools: {
'formattingfixer-fix': {
label: 'Fix Citations',
type: 'button',
oouiIcon: 'check',
action: {
type: 'callback',
execute: function() {
var textarea = document.getElementById('wpTextbox1');
var result = runActionOnWikitext( textarea.value, 'fix' );
textarea.value = result.wikitext;
if ( typeof $ !== 'undefined' ) {
$( textarea ).trigger( 'input' ).trigger( 'change' );
}
showResultMessage(result);
}
}
},
'formattingfixer-links': {
label: 'Auto Add Links',
type: 'button',
oouiIcon: 'link',
action: {
type: 'callback',
execute: function() {
var textarea = document.getElementById('wpTextbox1');
var result = runActionOnWikitext( textarea.value, 'links' );
textarea.value = result.wikitext;
if ( typeof $ !== 'undefined' ) {
$( textarea ).trigger( 'input' ).trigger( 'change' );
}
showResultMessage(result);
}
}
},
'formattingfixer-fixlinks': {
label: 'Fix Citations + Links',
type: 'button',
oouiIcon: 'link',
action: {
type: 'callback',
execute: function() {
var textarea = document.getElementById('wpTextbox1');
var result = runActionOnWikitext( textarea.value, 'fixlinks' );
textarea.value = result.wikitext;
if ( typeof $ !== 'undefined' ) {
$( textarea ).trigger( 'input' ).trigger( 'change' );
}
showResultMessage(result);
}
}
}
}
});
wikiEditorButtonsAdded = true;
console.log('FormattingFixer: WikiEditor button 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 = runActionOnWikitext( textbox.value, 'fix' );
textbox.value = result.wikitext;
if ( typeof $ !== 'undefined' ) {
$( textbox ).trigger( 'input' ).trigger( 'change' );
}
showResultMessage(result);
};
container.appendChild(fixBtn);
var linksBtn = document.createElement('button');
linksBtn.textContent = 'Auto Add Links';
linksBtn.style.cssText = 'padding: 5px 10px; cursor: pointer; border: 1px solid #a2a9b1; border-radius: 2px; background: #fff;';
linksBtn.type = 'button';
linksBtn.onclick = function() {
var result = runActionOnWikitext( textbox.value, 'links' );
textbox.value = result.wikitext;
if ( typeof $ !== 'undefined' ) {
$( textbox ).trigger( 'input' ).trigger( 'change' );
}
showResultMessage(result);
};
container.appendChild(linksBtn);
var bothBtn = document.createElement('button');
bothBtn.textContent = 'Fix Citations + Links';
bothBtn.style.cssText = 'padding: 5px 10px; cursor: pointer; border: 1px solid #a2a9b1; border-radius: 2px; background: #fff;';
bothBtn.type = 'button';
bothBtn.onclick = function() {
var result = runActionOnWikitext( textbox.value, 'fixlinks' );
textbox.value = result.wikitext;
if ( typeof $ !== 'undefined' ) {
$( textbox ).trigger( 'input' ).trigger( 'change' );
}
showResultMessage(result);
};
container.appendChild(bothBtn);
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,
generateRefName: generateRefName,
autoAddLinks: autoAddLinks
};
})();