MediaWiki:Gadget-FormattingFixer.js: Difference between revisions

From Candypedia
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 - ONLY for chapter URLs
     * Format a URL as proper citation
     */
     */
     function formatCitation(url) {
     function formatCitation(url) {
         if (!url) return null;
         if (!url) return null;
          
          
         // Only format chapter URLs (must start with /c followed by number)
         // 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 + '}}';
         }
         }
       
 
        // Don't touch other URLs (like /img/, /store/, etc.)
         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 - ONLY for chapter URLs
     * 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;
        var changeCount = 0;
          
          
         wikitext = wikitext.replace(refPattern, function(match, nameAttr, content) {
         wikitext = wikitext.replace(refPattern, function(match, nameAttr, content) {
Line 324: Line 329:
             }
             }
              
              
             // ONLY process chapter URLs (must have /cNUMBER/pNUMBER)
             // 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);
                 changeCount++;
                 changed++;
                 return '<ref' + nameAttr + '>' + formatted + '</ref>';
                 return '<ref' + nameAttr + '>' + formatted + '</ref>';
             }
             }
Line 335: Line 340:
         });
         });


         return { wikitext: wikitext, count: changeCount };
         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 (only chapter URLs)
         // 2. Standardize citation format
         var standardizeResult = standardizeCitations(wikitext);
         var citationResult = standardizeCitations(wikitext);
         if (standardizeResult.count > 0) {
         if (citationResult.wikitext !== wikitext) {
             wikitext = standardizeResult.wikitext;
             wikitext = citationResult.wikitext;
             fixes.push('Standardized ' + standardizeResult.count + ' raw URL(s) to {{Cite chapter}} format');
             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 with clean summary
     * Show result message using mw.notify (nicer than alert)
     */
     */
     function showResultMessage(result) {
     function showResultMessage(result) {
         var $content;
         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 (result.fixes.length === 0 && result.warnings.length === 0) {
        // Use mw.notify for a nicer notification, fall back to alert
             mw.notify('No issues found! Citations look good.', {
         if (typeof mw !== 'undefined' && mw.notify) {
             mw.notify( summary, {
                 title: 'FormattingFixer',
                 title: 'FormattingFixer',
                autoHide: true,
                type: getNotifyType( result ),
                 tag: 'formattingfixer'
                 tag: 'formattingfixer'
             });
             } );
            return;
         } else {
        }
             alert(summary);
       
        // Build a clean jQuery element for the notification
        $content = $('<div>');
       
        if (result.fixes.length > 0) {
            $content.append($('<strong>').text(result.fixes.length + ' fix(es) applied'));
            var $fixList = $('<ul>').css({ margin: '5px 0 10px 20px', padding: 0 });
            result.fixes.forEach(function(fix) {
                $fixList.append($('<li>').text(fix));
            });
            $content.append($fixList);
         }
       
        if (result.warnings.length > 0) {
             // Filter out empty warnings
            var realWarnings = result.warnings.filter(function(w) { return w.trim() !== '' && !w.startsWith('==='); });
            if (realWarnings.length > 0) {
                $content.append($('<strong>').css('color', '#d33').text(realWarnings.length + ' warning(s)'));
                var $warnList = $('<ul>').css({ margin: '5px 0 0 20px', padding: 0 });
                realWarnings.forEach(function(warn) {
                    $warnList.append($('<li>').text(warn));
                });
                $content.append($warnList);
            }
         }
         }
       
        mw.notify($content, {
            title: 'FormattingFixer',
            autoHide: false,
            tag: 'formattingfixer'
        });
    }
    /**
    * Auto Add Links stub - will be implemented later
    */
    function autoAddLinks(wikitext) {
        var fixes = [];
        var warnings = [];
       
        // TODO: Implement auto-linking logic
        // This will scan for unlinked character names, chapter titles, etc.
        // and automatically add wiki links [[Name]] around them.
       
        warnings.push('Auto Add Links is not yet implemented.');
       
        return {
            wikitext: wikitext,
            fixes: fixes,
            warnings: warnings
        };
    }
    /**
    * Combined function: Fix Citations + Auto Add Links
    */
    function fixAll(wikitext) {
        // First fix citations
        var citationResult = fixCitations(wikitext);
        wikitext = citationResult.wikitext;
       
        // Then auto add links
        var linkResult = autoAddLinks(wikitext);
        wikitext = linkResult.wikitext;
       
        // Combine results
        return {
            wikitext: wikitext,
            fixes: citationResult.fixes.concat(linkResult.fixes),
            warnings: citationResult.warnings.concat(linkResult.warnings)
        };
     }
     }


Line 597: Line 591:
         // way to apply whole-document wikitext transforms.
         // way to apply whole-document wikitext transforms.
         try {
         try {
             var switchPromise = target.switchToWikitextEditor( true );
             return target.switchToWikitextEditor( true );
            // If switchToWikitextEditor returns undefined, create our own promise
            if ( !switchPromise || typeof switchPromise.then !== 'function' ) {
                var deferred = $.Deferred();
                var attempt = 0;
                var check = function () {
                    attempt++;
                    var newSurface = veGetSurface();
                    if ( newSurface && veGetMode() === 'source' ) {
                        deferred.resolve();
                        return;
                    }
                    if ( attempt >= 20 ) {
                        deferred.reject( new Error( 'Mode switch failed' ) );
                        return;
                    }
                    setTimeout( check, 200 );
                };
                // Poll for up to ~4 seconds
                setTimeout( check, 0 );
                return deferred.promise();
            }
            return switchPromise;
         } catch ( e ) {
         } catch ( e ) {
             return $.Deferred().reject( e ).promise();
             return $.Deferred().reject( e ).promise();
Line 625: Line 597:
     }
     }


     /**
     function runFormattingFixerInVE( action ) {
    * Generic VE runner for any processing function
    */
    function runInVE( processFn ) {
         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 textbox = document.getElementById( 'wpTextbox1' );
             var sourceWikitext = veGetFullWikitextFromSourceSurface( surface );
             if ( textbox && !( textbox.classList && textbox.classList.contains( 've-dummyTextbox' ) ) ) {
             var result = runActionOnWikitext( sourceWikitext, action );
                var sourceWikitext = textbox.value;
            if ( result.wikitext !== sourceWikitext ) {
                var result = processFn( sourceWikitext );
                 veReplaceAllWikitextInSourceSurface( surface, result.wikitext );
                if ( result.wikitext !== sourceWikitext ) {
                    textbox.value = result.wikitext;
                    if ( typeof $ !== 'undefined' ) {
                        $( textbox ).trigger( 'input' );
                    } else {
                        textbox.dispatchEvent( new Event( 'input', { bubbles: true } ) );
                    }
                }
                showResultMessage( result );
                return;
            }
 
            // Fallback if #wpTextbox1 is not present
            var sourceSurfaceText = veGetFullWikitextFromSourceSurface( surface );
            var fallbackResult = processFn( sourceSurfaceText );
            if ( fallbackResult.wikitext !== sourceSurfaceText ) {
                 veReplaceAllWikitextInSourceSurface( surface, fallbackResult.wikitext );
             }
             }
             showResultMessage( fallbackResult );
             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 () {
         // Request switch, but do not rely on promise resolution (it can reject even
             var newSurface = veGetSurface();
        // when the UI switches successfully on some installs).
             if ( !newSurface || veGetMode() !== 'source' ) {
        try {
                mw.notify( 'FormattingFixer: Could not switch to source mode.', { type: 'error' } );
            if ( target && target.switchToWikitextEditor ) {
                target.switchToWikitextEditor( true );
            }
        } catch ( e ) {
            // Keep going; polling below will still work if the switch happens anyway.
        }
 
        // After switching, apply edits to the real wikitext textarea.
        // This is more reliable than VE document-model APIs for source mode.
        var attemptApplyToTextbox = function ( attempt ) {
            attempt = attempt || 1;
             var textbox = document.getElementById( 'wpTextbox1' );
             if ( textbox && !( textbox.classList && textbox.classList.contains( 've-dummyTextbox' ) ) ) {
                var wikitext = textbox.value;
                var result = processFn( wikitext );
                if ( result.wikitext !== wikitext ) {
                    textbox.value = result.wikitext;
                    if ( typeof $ !== 'undefined' ) {
                        $( textbox ).trigger( 'input' );
                    } else {
                        textbox.dispatchEvent( new Event( 'input', { bubbles: true } ) );
                    }
                }
                showResultMessage( result );
                 return;
                 return;
             }
             }
 
            var wikitext = veGetFullWikitextFromSourceSurface( newSurface );
             if ( attempt < 20 ) {
            var result = runActionOnWikitext( wikitext, action );
                 setTimeout( function () { attemptApplyToTextbox( attempt + 1 ); }, 200 );
             if ( result.wikitext !== wikitext ) {
                return;
                 veReplaceAllWikitextInSourceSurface( newSurface, result.wikitext );
             }
             }
 
            showResultMessage( result );
             mw.notify( 'FormattingFixer: Switched to source mode, but could not access the wikitext textarea.', { type: 'error' } );
        }, function () {
         };
             mw.notify( 'FormattingFixer: Could not switch to source mode. Please switch manually and try again.', { type: 'error' } );
 
         } );
        attemptApplyToTextbox( 1 );
     }
     }


Line 730: Line 658:
             var toolFactory = ve.ui.toolFactory;
             var toolFactory = ve.ui.toolFactory;


            // Tool 1: Fix Citations
             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 = 'reference';
                 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 () {
                     runInVE( fixCitations );
                     runFormattingFixerInVE( 'fix' );
                     this.setActive( false );
                     this.setActive( false );
                 };
                 };
Line 752: Line 679:
             }
             }


            // Tool 2: Auto Add Links
             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 () {
                     runInVE( autoAddLinks );
                     runFormattingFixerInVE( 'links' );
                     this.setActive( false );
                     this.setActive( false );
                 };
                 };
Line 774: Line 700:
             }
             }


            // Tool 3: Fix All (Citations + Links)
             if ( !toolFactory.lookup( 'formattingFixerFixLinks' ) ) {
             if ( !toolFactory.lookup( 'formattingFixerAll' ) ) {
                 function FormattingFixerFixLinksTool() {
                 function FormattingFixerAllTool() {
                     FormattingFixerFixLinksTool.super.apply( this, arguments );
                     FormattingFixerAllTool.super.apply( this, arguments );
                 }
                 }
                 OO.inheritClass( FormattingFixerAllTool, OO.ui.Tool );
                 OO.inheritClass( FormattingFixerFixLinksTool, OO.ui.Tool );
                 FormattingFixerAllTool.static.name = 'formattingFixerAll';
                 FormattingFixerFixLinksTool.static.name = 'formattingFixerFixLinks';
                 FormattingFixerAllTool.static.group = 'formattingfixer';
                 FormattingFixerFixLinksTool.static.group = 'formattingfixer';
                 FormattingFixerAllTool.static.title = 'Fix All';
                 FormattingFixerFixLinksTool.static.title = 'Fix Citations and Auto Add Links';
                 FormattingFixerAllTool.static.icon = 'checkAll';
                 FormattingFixerFixLinksTool.static.icon = 'link';
                 FormattingFixerAllTool.static.autoAddToCatchall = false;
                 FormattingFixerFixLinksTool.static.autoAddToCatchall = false;
                 FormattingFixerAllTool.static.autoAddToGroup = true;
                 FormattingFixerFixLinksTool.static.autoAddToGroup = true;
                 FormattingFixerAllTool.prototype.onSelect = function () {
                 FormattingFixerFixLinksTool.prototype.onSelect = function () {
                     runInVE( fixAll );
                     runFormattingFixerInVE( 'fixlinks' );
                     this.setActive( false );
                     this.setActive( false );
                 };
                 };
                 FormattingFixerAllTool.prototype.onUpdateState = function () {
                 FormattingFixerFixLinksTool.prototype.onUpdateState = function () {
                     this.setDisabled( false );
                     this.setDisabled( false );
                 };
                 };
                 toolFactory.register( FormattingFixerAllTool );
                 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', 'formattingFixerAll' ]
                 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: 'reference',
                         oouiIcon: 'check',
                         action: {
                         action: {
                             type: 'callback',
                             type: 'callback',
                             execute: function() {
                             execute: function() {
                                 var textarea = document.getElementById('wpTextbox1');
                                 var textarea = document.getElementById('wpTextbox1');
                                 var result = fixCitations(textarea.value);
                                 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);
                                // Trigger preview refresh if available
                                if (typeof $ !== 'undefined' && $('#wpPreview').length) {
                                    // Signal that content changed for live preview
                                    $(textarea).trigger('input');
                                }
                             }
                             }
                         }
                         }
Line 920: Line 843:
                             execute: function() {
                             execute: function() {
                                 var textarea = document.getElementById('wpTextbox1');
                                 var textarea = document.getElementById('wpTextbox1');
                                 var result = autoAddLinks(textarea.value);
                                 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);
                                $(textarea).trigger('input');
                             }
                             }
                         }
                         }
                     },
                     },
                     'formattingfixer-all': {
                     'formattingfixer-fixlinks': {
                         label: 'Fix All',
                         label: 'Fix Citations + Links',
                         type: 'button',
                         type: 'button',
                         oouiIcon: 'checkAll',
                         oouiIcon: 'link',
                         action: {
                         action: {
                             type: 'callback',
                             type: 'callback',
                             execute: function() {
                             execute: function() {
                                 var textarea = document.getElementById('wpTextbox1');
                                 var textarea = document.getElementById('wpTextbox1');
                                 var result = fixAll(textarea.value);
                                 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);
                                $(textarea).trigger('input');
                             }
                             }
                         }
                         }
Line 945: Line 872:
             });
             });
             wikiEditorButtonsAdded = true;
             wikiEditorButtonsAdded = true;
             console.log('FormattingFixer: WikiEditor buttons added');
             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 btnStyle = 'padding: 5px 10px; cursor: pointer; border: 1px solid #a2a9b1; border-radius: 2px; background: #fff;';


         var fixBtn = document.createElement('button');
         var fixBtn = document.createElement('button');
         fixBtn.textContent = 'Fix Citations';
         fixBtn.textContent = 'Fix Citations';
         fixBtn.style.cssText = btnStyle;
         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 = fixCitations(textbox.value);
             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 = btnStyle;
         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 = autoAddLinks(textbox.value);
             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 allBtn = document.createElement('button');
         var bothBtn = document.createElement('button');
         allBtn.textContent = 'Fix All';
         bothBtn.textContent = 'Fix Citations + Links';
         allBtn.style.cssText = btnStyle;
         bothBtn.style.cssText = 'padding: 5px 10px; cursor: pointer; border: 1px solid #a2a9b1; border-radius: 2px; background: #fff;';
         allBtn.type = 'button';
         bothBtn.type = 'button';
         allBtn.onclick = function() {
         bothBtn.onclick = function() {
             var result = fixAll(textbox.value);
             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(allBtn);
         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,
        autoAddLinks: autoAddLinks,
        fixAll: fixAll,
         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
    };

})();