MediaWiki:Gadget-FormattingFixer.js

From Candypedia
Revision as of 07:02, 5 January 2026 by Maintenance script (talk | contribs) (Add FormattingFixer gadget)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)

Note: After publishing, you may have to bypass your browser's cache to see the changes.

  • Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
  • Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
  • Internet Explorer / Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5
  • Opera: Press Ctrl-F5.
/**
 * FormattingFixer for MediaWiki
 * 
 * Fixes common reference citation issues in wikitext:
 * - Reorders refs so definitions come before reuses
 * - Standardizes chapter URLs to {{Cite chapter|url=...}} format
 * - Detects and helps fix undefined named refs
 * - Validates chapter URL format (must have /cXX/pXX)
 * 
 * Works with WikiEditor (source editing mode).
 * Note: Does NOT work with VisualEditor as it requires raw wikitext access.
 * 
 * Installation:
 * 1. Copy this code to MediaWiki:Gadget-FormattingFixer.js
 * 2. Add to MediaWiki:Gadgets-definition:
 *    * FormattingFixer[ResourceLoader|default|dependencies=ext.wikiEditor]|Gadget-FormattingFixer.js
 * 3. All users get it by default; can disable in Special:Preferences > Gadgets
 */
(function() {
    'use strict';

    // Configuration - adjust this pattern if your wiki uses a different URL structure
    var config = {
        chapterUrlPattern: /https?:\/\/www\.bittersweetcandybowl\.com\/c(\d+(?:\.\d+)?)\/p(\d+)/,
        chapterUrlLoose: /https?:\/\/www\.bittersweetcandybowl\.com\/c(\d+(?:\.\d+)?)(\/p\d+)?/,
        siteUrlPattern: /https?:\/\/www\.bittersweetcandybowl\.com\//
    };

    /**
     * Parse all references from wikitext
     */
    function parseRefs(wikitext) {
        var refs = {
            definitions: [],  // <ref name="X">content</ref>
            reuses: [],       // <ref name="X" />
            anonymous: []     // <ref>content</ref>
        };

        // Match named refs with content: <ref name="X">content</ref>
        var defined_refPattern = /<ref\s+name\s*=\s*["']([^"']+)["']\s*>([\s\S]*?)<\/ref>/gi;
        var match;
        while ((match = defined_refPattern.exec(wikitext)) !== null) {
            refs.definitions.push({
                fullMatch: match[0],
                name: match[1],
                content: match[2],
                index: match.index
            });
        }

        // Match named refs without content (reuses): <ref name="X" />
        var reusePattern = /<ref\s+name\s*=\s*["']([^"']+)["']\s*\/>/gi;
        while ((match = reusePattern.exec(wikitext)) !== null) {
            refs.reuses.push({
                fullMatch: match[0],
                name: match[1],
                index: match.index
            });
        }

        // Match anonymous refs: <ref>content</ref>
        // Need to be careful not to match named refs
        var anonPattern = /<ref\s*>([\s\S]*?)<\/ref>/gi;
        while ((match = anonPattern.exec(wikitext)) !== null) {
            // Double-check it's not a named ref that our pattern somehow caught
            if (!/<ref\s+name\s*=/.test(match[0])) {
                refs.anonymous.push({
                    fullMatch: match[0],
                    content: match[1],
                    index: match.index
                });
            }
        }

        return refs;
    }

    /**
     * Extract URL from ref content
     */
    function extractUrl(content) {
        // Try {{Cite chapter|url=...}} format first
        var citeMatch = /\{\{Cite\s*chapter\s*\|\s*url\s*=\s*([^\s\}\|]+)/i.exec(content);
        if (citeMatch) {
            return citeMatch[1];
        }
        
        // Try {{Cite web|url=...}} format
        var webMatch = /\{\{Cite\s*web\s*\|\s*url\s*=\s*([^\s\}\|]+)/i.exec(content);
        if (webMatch) {
            return webMatch[1];
        }

        // Try raw URL
        var urlMatch = /(https?:\/\/[^\s\}<]+)/.exec(content);
        if (urlMatch) {
            return urlMatch[1];
        }

        return null;
    }

    /**
     * Format a URL as proper citation
     */
    function formatCitation(url) {
        if (!url) return null;
        
        // Check if it's a chapter URL
        if (config.chapterUrlPattern.test(url)) {
            return '{{Cite chapter|url=' + url + '}}';
        }
        
        // For other site URLs, still use Cite chapter (adjust if you have other templates)
        if (config.siteUrlPattern.test(url)) {
            return '{{Cite chapter|url=' + url + '}}';
        }
        
        // For external URLs, could use Cite web
        return url;
    }

    /**
     * Validate a chapter URL has proper format
     */
    function validateChapterUrl(url) {
        if (!url) return { valid: false, error: 'No URL found' };
        
        var looseMatch = config.chapterUrlLoose.exec(url);
        if (!looseMatch) {
            return { valid: true, error: null }; // Not a chapter URL, skip validation
        }
        
        // It's a chapter URL - check if it has /pXX
        if (!looseMatch[2]) {
            return { 
                valid: false, 
                error: 'Chapter URL missing page number: ' + url + ' (needs /pXX)'
            };
        }
        
        return { valid: true, error: null };
    }

    /**
     * Find order issues - refs used before they're defined
     */
    function findOrderIssues(refs) {
        var issues = [];
        var definitionsByName = {};

        // Index definitions by name
        refs.definitions.forEach(function(def) {
            if (!definitionsByName[def.name]) {
                definitionsByName[def.name] = def;
            }
        });

        // Check each reuse
        refs.reuses.forEach(function(reuse) {
            var def = definitionsByName[reuse.name];
            if (def && reuse.index < def.index) {
                issues.push({
                    name: reuse.name,
                    reuseIndex: reuse.index,
                    definitionIndex: def.index,
                    definition: def,
                    reuse: reuse
                });
            }
        });

        return issues;
    }

    /**
     * Find undefined refs - names used but never defined
     */
    function findUndefinedRefs(refs) {
        var definedNames = new Set(refs.definitions.map(function(d) { return d.name; }));
        var undefinedRefs = [];

        refs.reuses.forEach(function(reuse) {
            if (!definedNames.has(reuse.name)) {
                // Check if we already logged this name
                if (!undefinedRefs.some(function(u) { return u.name === reuse.name; })) {
                    undefinedRefs.push({
                        name: reuse.name,
                        usages: refs.reuses.filter(function(r) { return r.name === reuse.name; })
                    });
                }
            }
        });

        return undefinedRefs;
    }

    /**
     * Find anonymous refs that could match undefined names
     */
    function findPotentialMatches(refs, undefinedRefs) {
        var matches = [];
        
        undefinedRefs.forEach(function(undef) {
            refs.anonymous.forEach(function(anon) {
                var url = extractUrl(anon.content);
                if (url) {
                    matches.push({
                        undefinedName: undef.name,
                        anonymousRef: anon,
                        url: url
                    });
                }
            });
        });

        return matches;
    }

    /**
     * Fix order issues in wikitext
     */
    function fixOrderIssues(wikitext, issues) {
        if (issues.length === 0) return wikitext;

        // Sort issues by definition index descending (fix from end to preserve indices)
        issues.sort(function(a, b) { return b.definitionIndex - a.definitionIndex; });

        issues.forEach(function(issue) {
            // Strategy: 
            // 1. Replace the definition with a reuse
            // 2. Replace the first reuse with the definition
            
            var def = issue.definition;
            var reuse = issue.reuse;
            
            // Build the replacement strings
            var reuseStr = '<ref name="' + def.name + '" />';
            var defStr = '<ref name="' + def.name + '">' + def.content + '</ref>';
            
            // Replace definition with reuse (do this first since it's later in the text)
            wikitext = wikitext.substring(0, def.index) + 
                       reuseStr + 
                       wikitext.substring(def.index + def.fullMatch.length);
            
            // Now replace the reuse with definition
            // Need to recalculate position since we changed the text
            var offset = reuseStr.length - def.fullMatch.length;
            var newReuseIndex = reuse.index; // reuse is before def, so no offset needed
            
            wikitext = wikitext.substring(0, newReuseIndex) + 
                       defStr + 
                       wikitext.substring(newReuseIndex + reuse.fullMatch.length);
        });

        return wikitext;
    }

    /**
     * Standardize citation format
     */
    function standardizeCitations(wikitext) {
        // Find all refs with raw URLs (not in Cite templates)
        var refPattern = /<ref(\s+name\s*=\s*["'][^"']+["'])?\s*>([\s\S]*?)<\/ref>/gi;
        
        wikitext = wikitext.replace(refPattern, function(match, nameAttr, content) {
            nameAttr = nameAttr || '';
            
            // Check if content is just a raw URL
            var trimmedContent = content.trim();
            
            // Skip if already in Cite template
            if (/\{\{Cite/i.test(trimmedContent)) {
                return match;
            }
            
            // Check if it's a raw URL to our site
            if (config.siteUrlPattern.test(trimmedContent)) {
                var url = trimmedContent;
                var formatted = formatCitation(url);
                return '<ref' + nameAttr + '>' + formatted + '</ref>';
            }
            
            return match;
        });

        return wikitext;
    }

    /**
     * Main fix function
     */
    function fixCitations(wikitext) {
        var issues = [];
        var warnings = [];
        var fixes = [];

        // Parse all refs
        var refs = parseRefs(wikitext);

        // 1. Find and fix order issues
        var orderIssues = findOrderIssues(refs);
        if (orderIssues.length > 0) {
            wikitext = fixOrderIssues(wikitext, orderIssues);
            orderIssues.forEach(function(issue) {
                fixes.push('Fixed order: "' + issue.name + '" - moved definition before first usage');
            });
            // Re-parse after fixes
            refs = parseRefs(wikitext);
        }

        // 2. Standardize citation format
        var before = wikitext;
        wikitext = standardizeCitations(wikitext);
        if (wikitext !== before) {
            fixes.push('Standardized raw URLs to {{Cite chapter|url=...}} format');
            refs = parseRefs(wikitext);
        }

        // 3. Find undefined refs
        var undefinedRefs = findUndefinedRefs(refs);
        if (undefinedRefs.length > 0) {
            undefinedRefs.forEach(function(undef) {
                warnings.push('Undefined reference: "' + undef.name + '" is used ' + 
                             undef.usages.length + ' time(s) but never defined');
            });
        }

        // 4. Validate chapter URLs
        refs.definitions.forEach(function(def) {
            var url = extractUrl(def.content);
            var validation = validateChapterUrl(url);
            if (!validation.valid) {
                warnings.push('Invalid URL in "' + def.name + '": ' + validation.error);
            }
        });
        refs.anonymous.forEach(function(anon, i) {
            var url = extractUrl(anon.content);
            var validation = validateChapterUrl(url);
            if (!validation.valid) {
                warnings.push('Invalid URL in anonymous ref #' + (i+1) + ': ' + validation.error);
            }
        });

        // 5. If there are undefined refs, try to help match them
        if (undefinedRefs.length > 0 && refs.anonymous.length > 0) {
            warnings.push('');
            warnings.push('=== Potential matches for undefined refs ===');
            warnings.push('Anonymous refs that could be assigned names:');
            refs.anonymous.forEach(function(anon, i) {
                var url = extractUrl(anon.content);
                warnings.push('  Anonymous #' + (i+1) + ': ' + (url || anon.content.substring(0, 50)));
            });
            warnings.push('');
            warnings.push('To fix: Add name="X" to the anonymous ref that should define each name.');
        }

        return {
            wikitext: wikitext,
            fixes: fixes,
            warnings: warnings
        };
    }

    /**
     * Interactive undefined ref fixer
     */
    function interactiveFixUndefined(wikitext) {
        var refs = parseRefs(wikitext);
        var undefinedRefs = findUndefinedRefs(refs);
        
        if (undefinedRefs.length === 0) {
            return { wikitext: wikitext, message: 'No undefined references found.' };
        }

        // For each undefined ref, prompt user to select an anonymous ref
        undefinedRefs.forEach(function(undef) {
            var options = ['[Skip - leave undefined]', '[Create placeholder]'];
            refs.anonymous.forEach(function(anon, i) {
                var url = extractUrl(anon.content);
                options.push('Anonymous #' + (i+1) + ': ' + (url || anon.content.substring(0, 60)));
            });

            var message = 'Reference "' + undef.name + '" is used but never defined.\n\n' +
                         'Select which anonymous ref should define it:\n\n' +
                         options.map(function(o, i) { return i + ': ' + o; }).join('\n');
            
            var choice = prompt(message, '0');
            if (choice === null) return; // Cancelled
            
            var choiceNum = parseInt(choice, 10);
            
            if (choiceNum === 1) {
                // Create placeholder - find first usage and replace with definition
                var firstUsage = undef.usages[0];
                var placeholder = '<ref name="' + undef.name + '">{{Cite chapter|url=UNKNOWN_URL_PLEASE_FIX}}</ref>';
                wikitext = wikitext.substring(0, firstUsage.index) + 
                           placeholder + 
                           wikitext.substring(firstUsage.index + firstUsage.fullMatch.length);
            } else if (choiceNum >= 2) {
                // Assign name to selected anonymous ref
                var anonIndex = choiceNum - 2;
                var anon = refs.anonymous[anonIndex];
                if (anon) {
                    var namedRef = '<ref name="' + undef.name + '">' + anon.content + '</ref>';
                    wikitext = wikitext.substring(0, anon.index) + 
                               namedRef + 
                               wikitext.substring(anon.index + anon.fullMatch.length);
                    // Re-parse since we modified the text
                    refs = parseRefs(wikitext);
                }
            }
        });

        return { wikitext: wikitext, message: 'Interactive fix complete.' };
    }

    /**
     * Add toolbar button for WikiEditor
     */
    function addToolbarButton() {
        if (typeof $ === 'undefined' || !$.fn.wikiEditor) {
            console.log('WikiEditor not available');
            return;
        }

        $('#wpTextbox1').wikiEditor('addToToolbar', {
            section: 'advanced',
            group: 'format',
            tools: {
                'fix-citations': {
                    label: 'Fix Citations',
                    type: 'button',
                    icon: '//upload.wikimedia.org/wikipedia/commons/thumb/4/4a/Commons-emblem-success.svg/22px-Commons-emblem-success.svg.png',
                    action: {
                        type: 'callback',
                        execute: function(context) {
                            var textarea = document.getElementById('wpTextbox1');
                            var result = fixCitations(textarea.value);
                            
                            var message = '';
                            if (result.fixes.length > 0) {
                                message += 'FIXES APPLIED:\n' + result.fixes.join('\n') + '\n\n';
                            }
                            if (result.warnings.length > 0) {
                                message += 'WARNINGS:\n' + result.warnings.join('\n');
                            }
                            if (result.fixes.length === 0 && result.warnings.length === 0) {
                                message = 'No issues found! Citations look good.';
                            }
                            
                            textarea.value = result.wikitext;
                            alert(message);
                        }
                    }
                },
                'fix-citations-interactive': {
                    label: 'Fix Undefined Refs',
                    type: 'button',
                    icon: '//upload.wikimedia.org/wikipedia/commons/thumb/e/ea/Disambig_colour.svg/22px-Disambig_colour.svg.png',
                    action: {
                        type: 'callback',
                        execute: function(context) {
                            var textarea = document.getElementById('wpTextbox1');
                            var result = interactiveFixUndefined(textarea.value);
                            textarea.value = result.wikitext;
                            alert(result.message);
                        }
                    }
                }
            }
        });
    }

    /**
     * Alternative: Add to page as simple buttons (for wikis without WikiEditor)
     */
    function addSimpleButtons() {
        var toolbar = document.getElementById('toolbar') || 
                      document.querySelector('.wikiEditor-ui-toolbar') ||
                      document.querySelector('#wpTextbox1')?.parentNode;
        
        if (!toolbar) return;

        var container = document.createElement('div');
        container.style.cssText = 'margin: 5px 0; padding: 5px; background: #f8f9fa; border: 1px solid #a2a9b1; border-radius: 2px;';
        
        var fixBtn = document.createElement('button');
        fixBtn.textContent = '🔧 Fix Citations';
        fixBtn.style.cssText = 'margin-right: 10px; padding: 5px 10px; cursor: pointer;';
        fixBtn.type = 'button';
        fixBtn.onclick = function() {
            var textarea = document.getElementById('wpTextbox1');
            var result = fixCitations(textarea.value);
            
            var message = '';
            if (result.fixes.length > 0) {
                message += 'FIXES APPLIED:\n' + result.fixes.join('\n') + '\n\n';
            }
            if (result.warnings.length > 0) {
                message += 'WARNINGS:\n' + result.warnings.join('\n');
            }
            if (result.fixes.length === 0 && result.warnings.length === 0) {
                message = 'No issues found! Citations look good.';
            }
            
            textarea.value = result.wikitext;
            alert(message);
        };

        var interactiveBtn = document.createElement('button');
        interactiveBtn.textContent = '🔗 Fix Undefined Refs';
        interactiveBtn.style.cssText = 'padding: 5px 10px; cursor: pointer;';
        interactiveBtn.type = 'button';
        interactiveBtn.onclick = function() {
            var textarea = document.getElementById('wpTextbox1');
            var result = interactiveFixUndefined(textarea.value);
            textarea.value = result.wikitext;
            alert(result.message);
        };

        container.appendChild(fixBtn);
        container.appendChild(interactiveBtn);
        
        var textbox = document.getElementById('wpTextbox1');
        if (textbox) {
            textbox.parentNode.insertBefore(container, textbox);
        }
    }

    /**
     * Initialize - hook into WikiEditor when ready
     */
    function init() {
        // Only run on edit pages (source editor, not VisualEditor)
        var action = mw.config.get('wgAction');
        if (action !== 'edit' && action !== 'submit') {
            return;
        }

        // Wait for WikiEditor to be ready, then add toolbar buttons
        mw.hook('wikiEditor.toolbarReady').add(function($textarea) {
            addToolbarButton();
        });

        // Fallback: if WikiEditor hook doesn't fire, try after a delay
        setTimeout(function() {
            if (!window._formattingFixerInitialized) {
                // Check if WikiEditor toolbar exists
                if ($('.wikiEditor-ui-toolbar').length > 0) {
                    addToolbarButton();
                } else {
                    // No WikiEditor, add simple buttons
                    addSimpleButtons();
                }
            }
        }, 2000);
    }

    // Mark as initialized when toolbar button is added
    var originalAddToolbarButton = addToolbarButton;
    addToolbarButton = function() {
        window._formattingFixerInitialized = true;
        originalAddToolbarButton();
    };

    // Run init when DOM and MediaWiki are ready
    $(function() {
        init();
    });

    // Expose for testing
    window.FormattingFixer = {
        fixCitations: fixCitations,
        parseRefs: parseRefs,
        interactiveFixUndefined: interactiveFixUndefined
    };

})();